Skip to content

Commit

Permalink
feat: add link preview for status list item #18
Browse files Browse the repository at this point in the history
feat: add link preview for status list item
  • Loading branch information
whitescent authored Jan 24, 2024
2 parents 0138565 + ce582d2 commit bff4b6b
Show file tree
Hide file tree
Showing 15 changed files with 691 additions and 48 deletions.
435 changes: 435 additions & 0 deletions app/schemas/com.github.whitescent.mastify.database.AppDatabase/6.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package com.github.whitescent.mastify.data.model.ui
import androidx.compose.runtime.Immutable
import com.github.whitescent.mastify.network.model.account.Account
import com.github.whitescent.mastify.network.model.emoji.Emoji
import com.github.whitescent.mastify.network.model.status.Card
import com.github.whitescent.mastify.network.model.status.Poll
import com.github.whitescent.mastify.network.model.status.Status
import kotlinx.collections.immutable.ImmutableList
Expand Down Expand Up @@ -49,6 +50,7 @@ data class StatusUiData(
val createdAt: String,
val sensitive: Boolean,
val poll: Poll?,
val card: Card?,
val spoilerText: String,
val repliesCount: Int,
val reblogsCount: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@ import com.github.whitescent.mastify.database.util.Converters
AccountEntity::class,
InstanceEntity::class
],
version = 5,
version = 6,
autoMigrations = [
AutoMigration(from = 1, to = 2),
AutoMigration(from = 2, to = 3),
AutoMigration(from = 3, to = 4),
AutoMigration(from = 4, to = 5),
AutoMigration(from = 5, to = 6),
],
)
@TypeConverters(Converters::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import androidx.room.Index
import androidx.room.PrimaryKey
import com.github.whitescent.mastify.network.model.account.Account
import com.github.whitescent.mastify.network.model.emoji.Emoji
import com.github.whitescent.mastify.network.model.status.Card
import com.github.whitescent.mastify.network.model.status.Hashtag
import com.github.whitescent.mastify.network.model.status.Poll
import com.github.whitescent.mastify.network.model.status.Status
Expand Down Expand Up @@ -63,6 +64,7 @@ data class TimelineEntity(
@ColumnInfo val content: String,
@ColumnInfo val account: Account,
@ColumnInfo val poll: Poll?,
@ColumnInfo val card: Card?,
@ColumnInfo val emojis: List<Emoji>,
@ColumnInfo val tags: List<Hashtag>,
@ColumnInfo val mentions: List<Mention>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,26 @@

package com.github.whitescent.mastify.database.util

import androidx.room.ProvidedTypeConverter
import androidx.room.TypeConverter
import com.github.whitescent.mastify.network.model.account.Account
import com.github.whitescent.mastify.network.model.account.Fields
import com.github.whitescent.mastify.network.model.emoji.Emoji
import com.github.whitescent.mastify.network.model.status.Card
import com.github.whitescent.mastify.network.model.status.Hashtag
import com.github.whitescent.mastify.network.model.status.Poll
import com.github.whitescent.mastify.network.model.status.Status
import com.github.whitescent.mastify.network.model.status.Status.Application
import com.github.whitescent.mastify.network.model.status.Status.Attachment
import com.github.whitescent.mastify.network.model.status.Status.Mention
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import javax.inject.Inject
import javax.inject.Singleton

class Converters {

@OptIn(ExperimentalSerializationApi::class)
private val json = Json {
ignoreUnknownKeys = true
explicitNulls = false
}
@ProvidedTypeConverter
@Singleton
class Converters @Inject constructor(private val json: Json) {

@TypeConverter
fun jsonToStatus(json: String): Status = this.json.decodeFromString(json)
Expand Down Expand Up @@ -92,4 +91,10 @@ class Converters {

@TypeConverter
fun pollToJson(poll: Poll?): String = json.encodeToString(poll)

@TypeConverter
fun jsonToCard(json: String): Card = this.json.decodeFromString(json)

@TypeConverter
fun cardToJson(card: Card): String = json.encodeToString(card)
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package com.github.whitescent.mastify.di
import android.content.Context
import androidx.room.Room
import com.github.whitescent.mastify.database.AppDatabase
import com.github.whitescent.mastify.database.util.Converters
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
Expand All @@ -30,13 +31,17 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {

@Provides
@Singleton
fun providesNiaDatabase(
@ApplicationContext context: Context,
converters: Converters
): AppDatabase = Room.databaseBuilder(
context,
AppDatabase::class.java,
"mastify-database",
).build()
)
.addTypeConverter(converters)
.build()
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ fun Status.toUiData(): StatusUiData = StatusUiData(
createdAt = reblog?.createdAt ?: createdAt,
accountEmojis = (reblog?.account?.emojis ?: account.emojis).toImmutableList(),
emojis = (reblog?.emojis ?: emojis).toImmutableList(),
card = (reblog?.card ?: card)?.let {
it.copy(
title = it.title.trim(),
description = it.description.trim()
)
},
displayName = generateHtmlContentWithEmoji(
reblog?.account?.realDisplayName ?: account.realDisplayName,
reblog?.account?.emojis ?: account.emojis
Expand Down Expand Up @@ -108,7 +114,8 @@ fun Status.toEntity(timelineUserId: Long): TimelineEntity {
account = account,
application = application,
attachments = attachments,
hasUnloadedStatus = hasUnloadedStatus
hasUnloadedStatus = hasUnloadedStatus,
card = card
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2024 WhiteScent
*
* This file is a part of Mastify.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Mastify is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Mastify; if not,
* see <http://www.gnu.org/licenses>.
*/

package com.github.whitescent.mastify.network.model.status

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class Card(
val url: String,
val title: String,
val description: String,
@SerialName("author_name") val authorName: String,
val image: String?,
val type: String,
val width: Int,
val height: Int,
val blurhash: String?,
@SerialName("embed_url") val embedUrl: String?
)
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ data class Poll(
@SerialName("votes_count") val votesCount: Int,
@SerialName("voters_count") val votersCount: Int?, // nullable for compatibility with other fediverse
val options: List<PollOption>,
val emojis: List<Emoji>,
val emojis: List<Emoji> = emptyList(),
val voted: Boolean,
@SerialName("own_votes") val ownVotes: List<Int>?
) : Parcelable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ data class Status(
val content: String,
val account: Account,
val poll: Poll?,
val card: Card?,
val emojis: List<Emoji>,
val tags: List<Hashtag>,
val mentions: List<Mention>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,11 @@ fun StatusDetailCard(
}
}
else -> {
Column {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.padding(top = 8.dp)
) {
if (status.content.isNotEmpty()) {
HeightSpacer(value = 4.dp)
SelectionContainer {
HtmlText(
text = status.content,
Expand All @@ -187,11 +189,11 @@ fun StatusDetailCard(
)
}
}
StatusPoll(status.poll, Modifier.padding(top = 8.dp)) { id, choices ->
StatusLinkPreviewCard(card = status.card)
StatusPoll(status.poll) { id, choices ->
action(StatusAction.VotePoll(id, choices, status.actionable))
}
if (displayAttachments.isNotEmpty()) {
HeightSpacer(value = 4.dp)
StatusMedia(
attachments = displayAttachments,
onClick = { targetIndex ->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* Copyright 2024 WhiteScent
*
* This file is a part of Mastify.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Mastify is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Mastify; if not,
* see <http://www.gnu.org/licenses>.
*/

package com.github.whitescent.mastify.ui.component.status

import android.net.Uri
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import com.github.whitescent.R
import com.github.whitescent.mastify.network.model.status.Card
import com.github.whitescent.mastify.ui.component.CenterRow
import com.github.whitescent.mastify.ui.component.HeightSpacer
import com.github.whitescent.mastify.ui.component.WidthSpacer
import com.github.whitescent.mastify.ui.theme.AppTheme
import com.github.whitescent.mastify.utils.launchCustomChromeTab

@Composable
fun StatusLinkPreviewCard(
card: Card?,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
val toolbarColor = AppTheme.colors.primaryContent
if (card != null) {
Card(
modifier = modifier.fillMaxWidth(),
onClick = {
launchCustomChromeTab(
context = context,
uri = Uri.parse(card.url),
toolbarColor = toolbarColor.toArgb(),
)
},
shape = AppTheme.shape.mediumAvatar,
colors = CardDefaults.elevatedCardColors(
containerColor = AppTheme.colors.cardBackground,
contentColor = AppTheme.colors.primaryContent
),
border = BorderStroke(1.dp, AppTheme.colors.divider)
) {
CenterRow(Modifier.fillMaxWidth().heightIn(max = 100.dp)) {
if (!card.image.isNullOrEmpty()) {
AsyncImage(
model = card.image,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxHeight().width(100.dp)
)
WidthSpacer(value = 6.dp)
}
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 10.dp, vertical = 12.dp),
verticalArrangement = Arrangement.Center
) {
when {
card.title.isNotBlank() && card.description.isNotBlank() -> {
Text(
text = card.title,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
HeightSpacer(value = 4.dp)
Text(
text = card.description,
fontSize = 12.sp,
color = AppTheme.colors.primaryContent.copy(0.6f),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
}
card.title.isNotBlank() -> Text(
text = card.title,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
card.description.isNotBlank() -> Text(
text = card.description,
fontSize = 12.sp,
color = AppTheme.colors.primaryContent.copy(0.6f),
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
}
if (card.image.isNullOrEmpty()) {
CenterRow(Modifier.align(Alignment.End)) {
Icon(
painter = painterResource(id = R.drawable.link_simple),
contentDescription = null,
tint = AppTheme.colors.accent.copy(0.8f),
modifier = Modifier.size(20.dp)
)
WidthSpacer(value = 2.dp)
Text(
text = stringResource(id = R.string.link_preview_title),
fontSize = 12.sp,
color = AppTheme.colors.primaryContent.copy(0.6f),
)
}
}
}
}
}
}
}
Loading

0 comments on commit bff4b6b

Please sign in to comment.