refactor: Replace .to... with .from() in most cases (#82)
The previous code generally converted between a higher and a lower type by putting the type conversion functions on the lower type. This introduced cycles in the code dependency graph, and made it more difficult to follow the code flow. Refactor the code so that types generally have a `from(...)` static factory method that can create an instance from a lower type, and if appropriate a `to...()` method that can also create an instance of that lower type. Add `docs/code-style.md` which explains the rationale for this change in more detail so that future contributors can write code in the same style.
This commit is contained in:
parent
f45a3df83f
commit
3a274b0594
|
@ -53,6 +53,7 @@ import app.pachli.entity.Emoji;
|
||||||
import app.pachli.entity.Filter;
|
import app.pachli.entity.Filter;
|
||||||
import app.pachli.entity.FilterResult;
|
import app.pachli.entity.FilterResult;
|
||||||
import app.pachli.entity.HashTag;
|
import app.pachli.entity.HashTag;
|
||||||
|
import app.pachli.entity.Poll;
|
||||||
import app.pachli.entity.Status;
|
import app.pachli.entity.Status;
|
||||||
import app.pachli.interfaces.StatusActionListener;
|
import app.pachli.interfaces.StatusActionListener;
|
||||||
import app.pachli.util.AbsoluteTimeFormatter;
|
import app.pachli.util.AbsoluteTimeFormatter;
|
||||||
|
@ -279,7 +280,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
List<Status.Mention> mentions = actionable.getMentions();
|
List<Status.Mention> mentions = actionable.getMentions();
|
||||||
List<HashTag> tags =actionable.getTags();
|
List<HashTag> tags =actionable.getTags();
|
||||||
List<Emoji> emojis = actionable.getEmojis();
|
List<Emoji> emojis = actionable.getEmojis();
|
||||||
PollViewData poll = PollViewDataKt.toViewData(actionable.getPoll());
|
Poll poll = actionable.getPoll();
|
||||||
|
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
CharSequence emojifiedText = CustomEmojiHelper.emojify(content, emojis, this.content, statusDisplayOptions.animateEmojis());
|
CharSequence emojifiedText = CustomEmojiHelper.emojify(content, emojis, this.content, statusDisplayOptions.animateEmojis());
|
||||||
|
@ -288,7 +289,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
updateMediaLabel(i, sensitive, true);
|
updateMediaLabel(i, sensitive, true);
|
||||||
}
|
}
|
||||||
if (poll != null) {
|
if (poll != null) {
|
||||||
setupPoll(poll, emojis, statusDisplayOptions, listener);
|
setupPoll(PollViewData.Companion.from(poll), emojis, statusDisplayOptions, listener);
|
||||||
} else {
|
} else {
|
||||||
hidePoll();
|
hidePoll();
|
||||||
}
|
}
|
||||||
|
@ -962,24 +963,28 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
private CharSequence getPollDescription(@NonNull StatusViewData status,
|
private CharSequence getPollDescription(@NonNull StatusViewData status,
|
||||||
@NonNull Context context,
|
@NonNull Context context,
|
||||||
@NonNull StatusDisplayOptions statusDisplayOptions) {
|
@NonNull StatusDisplayOptions statusDisplayOptions) {
|
||||||
PollViewData poll = PollViewDataKt.toViewData(status.getActionable().getPoll());
|
Poll poll = status.getActionable().getPoll();
|
||||||
if (poll == null) {
|
if (poll == null) {
|
||||||
return "";
|
return "";
|
||||||
} else {
|
|
||||||
Object[] args = new CharSequence[5];
|
|
||||||
List<PollOptionViewData> options = poll.getOptions();
|
|
||||||
for (int i = 0; i < args.length; i++) {
|
|
||||||
if (i < options.size()) {
|
|
||||||
int percent = PollViewDataKt.calculatePercent(options.get(i).getVotesCount(), poll.getVotersCount(), poll.getVotesCount());
|
|
||||||
args[i] = buildDescription(options.get(i).getTitle(), percent, options.get(i).getVoted(), context);
|
|
||||||
} else {
|
|
||||||
args[i] = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
args[4] = getPollInfoText(System.currentTimeMillis(), poll, statusDisplayOptions,
|
|
||||||
context);
|
|
||||||
return context.getString(R.string.description_poll, args);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PollViewData pollViewData = PollViewData.Companion.from(poll);
|
||||||
|
Object[] args = new CharSequence[5];
|
||||||
|
List<PollOptionViewData> options = pollViewData.getOptions();
|
||||||
|
int totalVotes = pollViewData.getVotesCount();
|
||||||
|
Integer totalVoters = pollViewData.getVotersCount();
|
||||||
|
|
||||||
|
for (int i = 0; i < args.length; i++) {
|
||||||
|
if (i < options.size()) {
|
||||||
|
int percent = PollViewDataKt.calculatePercent(options.get(i).getVotesCount(), totalVoters, totalVotes);
|
||||||
|
args[i] = buildDescription(options.get(i).getTitle(), percent, options.get(i).getVoted(), context);
|
||||||
|
} else {
|
||||||
|
args[i] = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
args[4] = getPollInfoText(System.currentTimeMillis(), pollViewData, statusDisplayOptions,
|
||||||
|
context);
|
||||||
|
return context.getString(R.string.description_poll, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
|
|
@ -26,7 +26,6 @@ import app.pachli.entity.HashTag
|
||||||
import app.pachli.entity.Poll
|
import app.pachli.entity.Poll
|
||||||
import app.pachli.entity.Status
|
import app.pachli.entity.Status
|
||||||
import app.pachli.entity.TimelineAccount
|
import app.pachli.entity.TimelineAccount
|
||||||
import app.pachli.viewdata.StatusViewData
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
@Entity(primaryKeys = ["id", "accountId"])
|
@Entity(primaryKeys = ["id", "accountId"])
|
||||||
|
@ -39,13 +38,26 @@ data class ConversationEntity(
|
||||||
val unread: Boolean,
|
val unread: Boolean,
|
||||||
@Embedded(prefix = "s_") val lastStatus: ConversationStatusEntity,
|
@Embedded(prefix = "s_") val lastStatus: ConversationStatusEntity,
|
||||||
) {
|
) {
|
||||||
fun toViewData(): ConversationViewData {
|
companion object {
|
||||||
return ConversationViewData(
|
fun from(
|
||||||
id = id,
|
conversation: Conversation,
|
||||||
|
accountId: Long,
|
||||||
|
order: Int,
|
||||||
|
expanded: Boolean,
|
||||||
|
contentShowing: Boolean,
|
||||||
|
contentCollapsed: Boolean,
|
||||||
|
) = ConversationEntity(
|
||||||
|
accountId = accountId,
|
||||||
|
id = conversation.id,
|
||||||
order = order,
|
order = order,
|
||||||
accounts = accounts,
|
accounts = conversation.accounts.map { ConversationAccountEntity.from(it) },
|
||||||
unread = unread,
|
unread = conversation.unread,
|
||||||
lastStatus = lastStatus.toViewData(),
|
lastStatus = ConversationStatusEntity.from(
|
||||||
|
conversation.lastStatus!!,
|
||||||
|
expanded = expanded,
|
||||||
|
contentShowing = contentShowing,
|
||||||
|
contentCollapsed = contentCollapsed,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,6 +82,17 @@ data class ConversationAccountEntity(
|
||||||
emojis = emojis,
|
emojis = emojis,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(timelineAccount: TimelineAccount) = ConversationAccountEntity(
|
||||||
|
id = timelineAccount.id,
|
||||||
|
localUsername = timelineAccount.localUsername,
|
||||||
|
username = timelineAccount.username,
|
||||||
|
displayName = timelineAccount.name,
|
||||||
|
avatar = timelineAccount.avatar,
|
||||||
|
emojis = timelineAccount.emojis.orEmpty(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
|
@ -100,104 +123,37 @@ data class ConversationStatusEntity(
|
||||||
val language: String?,
|
val language: String?,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun toViewData(): StatusViewData {
|
companion object {
|
||||||
return StatusViewData(
|
fun from(
|
||||||
status = Status(
|
status: Status,
|
||||||
id = id,
|
expanded: Boolean,
|
||||||
url = url,
|
contentShowing: Boolean,
|
||||||
account = account.toAccount(),
|
contentCollapsed: Boolean,
|
||||||
inReplyToId = inReplyToId,
|
) = ConversationStatusEntity(
|
||||||
inReplyToAccountId = inReplyToAccountId,
|
id = status.id,
|
||||||
content = content,
|
url = status.url,
|
||||||
reblog = null,
|
inReplyToId = status.inReplyToId,
|
||||||
createdAt = createdAt,
|
inReplyToAccountId = status.inReplyToAccountId,
|
||||||
editedAt = editedAt,
|
account = ConversationAccountEntity.from(status.account),
|
||||||
emojis = emojis,
|
content = status.content,
|
||||||
reblogsCount = 0,
|
createdAt = status.createdAt,
|
||||||
favouritesCount = favouritesCount,
|
editedAt = status.editedAt,
|
||||||
repliesCount = repliesCount,
|
emojis = status.emojis,
|
||||||
reblogged = false,
|
favouritesCount = status.favouritesCount,
|
||||||
favourited = favourited,
|
repliesCount = status.repliesCount,
|
||||||
bookmarked = bookmarked,
|
favourited = status.favourited,
|
||||||
sensitive = sensitive,
|
bookmarked = status.bookmarked,
|
||||||
spoilerText = spoilerText,
|
sensitive = status.sensitive,
|
||||||
visibility = Status.Visibility.DIRECT,
|
spoilerText = status.spoilerText,
|
||||||
attachments = attachments,
|
attachments = status.attachments,
|
||||||
mentions = mentions,
|
mentions = status.mentions,
|
||||||
tags = tags,
|
tags = status.tags,
|
||||||
application = null,
|
showingHiddenContent = contentShowing,
|
||||||
pinned = false,
|
expanded = expanded,
|
||||||
muted = muted,
|
collapsed = contentCollapsed,
|
||||||
poll = poll,
|
muted = status.muted ?: false,
|
||||||
card = null,
|
poll = status.poll,
|
||||||
language = language,
|
language = status.language,
|
||||||
filtered = null,
|
|
||||||
),
|
|
||||||
isExpanded = expanded,
|
|
||||||
isShowingContent = showingHiddenContent,
|
|
||||||
isCollapsed = collapsed,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun TimelineAccount.toEntity() =
|
|
||||||
ConversationAccountEntity(
|
|
||||||
id = id,
|
|
||||||
localUsername = localUsername,
|
|
||||||
username = username,
|
|
||||||
displayName = name,
|
|
||||||
avatar = avatar,
|
|
||||||
emojis = emojis.orEmpty(),
|
|
||||||
)
|
|
||||||
|
|
||||||
fun Status.toEntity(
|
|
||||||
expanded: Boolean,
|
|
||||||
contentShowing: Boolean,
|
|
||||||
contentCollapsed: Boolean,
|
|
||||||
) =
|
|
||||||
ConversationStatusEntity(
|
|
||||||
id = id,
|
|
||||||
url = url,
|
|
||||||
inReplyToId = inReplyToId,
|
|
||||||
inReplyToAccountId = inReplyToAccountId,
|
|
||||||
account = account.toEntity(),
|
|
||||||
content = content,
|
|
||||||
createdAt = createdAt,
|
|
||||||
editedAt = editedAt,
|
|
||||||
emojis = emojis,
|
|
||||||
favouritesCount = favouritesCount,
|
|
||||||
repliesCount = repliesCount,
|
|
||||||
favourited = favourited,
|
|
||||||
bookmarked = bookmarked,
|
|
||||||
sensitive = sensitive,
|
|
||||||
spoilerText = spoilerText,
|
|
||||||
attachments = attachments,
|
|
||||||
mentions = mentions,
|
|
||||||
tags = tags,
|
|
||||||
showingHiddenContent = contentShowing,
|
|
||||||
expanded = expanded,
|
|
||||||
collapsed = contentCollapsed,
|
|
||||||
muted = muted ?: false,
|
|
||||||
poll = poll,
|
|
||||||
language = language,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun Conversation.toEntity(
|
|
||||||
accountId: Long,
|
|
||||||
order: Int,
|
|
||||||
expanded: Boolean,
|
|
||||||
contentShowing: Boolean,
|
|
||||||
contentCollapsed: Boolean,
|
|
||||||
) =
|
|
||||||
ConversationEntity(
|
|
||||||
accountId = accountId,
|
|
||||||
id = id,
|
|
||||||
order = order,
|
|
||||||
accounts = accounts.map { it.toEntity() },
|
|
||||||
unread = unread,
|
|
||||||
lastStatus = lastStatus!!.toEntity(
|
|
||||||
expanded = expanded,
|
|
||||||
contentShowing = contentShowing,
|
|
||||||
contentCollapsed = contentCollapsed,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ data class ConversationViewData(
|
||||||
val unread: Boolean,
|
val unread: Boolean,
|
||||||
val lastStatus: StatusViewData,
|
val lastStatus: StatusViewData,
|
||||||
) {
|
) {
|
||||||
fun toEntity(
|
fun toConversationEntity(
|
||||||
accountId: Long,
|
accountId: Long,
|
||||||
favourited: Boolean = lastStatus.status.favourited,
|
favourited: Boolean = lastStatus.status.favourited,
|
||||||
bookmarked: Boolean = lastStatus.status.bookmarked,
|
bookmarked: Boolean = lastStatus.status.bookmarked,
|
||||||
|
@ -52,41 +52,14 @@ data class ConversationViewData(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun StatusViewData.toConversationStatusEntity(
|
companion object {
|
||||||
favourited: Boolean = status.favourited,
|
fun from(conversationEntity: ConversationEntity) = ConversationViewData(
|
||||||
bookmarked: Boolean = status.bookmarked,
|
id = conversationEntity.id,
|
||||||
muted: Boolean = status.muted ?: false,
|
order = conversationEntity.order,
|
||||||
poll: Poll? = status.poll,
|
accounts = conversationEntity.accounts,
|
||||||
expanded: Boolean = isExpanded,
|
unread = conversationEntity.unread,
|
||||||
collapsed: Boolean = isCollapsed,
|
lastStatus = StatusViewData.from(conversationEntity.lastStatus),
|
||||||
showingHiddenContent: Boolean = isShowingContent,
|
)
|
||||||
): ConversationStatusEntity {
|
}
|
||||||
return ConversationStatusEntity(
|
|
||||||
id = id,
|
|
||||||
url = status.url,
|
|
||||||
inReplyToId = status.inReplyToId,
|
|
||||||
inReplyToAccountId = status.inReplyToAccountId,
|
|
||||||
account = status.account.toEntity(),
|
|
||||||
content = status.content,
|
|
||||||
createdAt = status.createdAt,
|
|
||||||
editedAt = status.editedAt,
|
|
||||||
emojis = status.emojis,
|
|
||||||
favouritesCount = status.favouritesCount,
|
|
||||||
repliesCount = status.repliesCount,
|
|
||||||
favourited = favourited,
|
|
||||||
bookmarked = bookmarked,
|
|
||||||
sensitive = status.sensitive,
|
|
||||||
spoilerText = status.spoilerText,
|
|
||||||
attachments = status.attachments,
|
|
||||||
mentions = status.mentions,
|
|
||||||
tags = status.tags,
|
|
||||||
showingHiddenContent = showingHiddenContent,
|
|
||||||
expanded = expanded,
|
|
||||||
collapsed = collapsed,
|
|
||||||
muted = muted,
|
|
||||||
poll = poll,
|
|
||||||
language = status.language,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,17 +58,13 @@ class ConversationsRemoteMediator(
|
||||||
conversations
|
conversations
|
||||||
.filterNot { it.lastStatus == null }
|
.filterNot { it.lastStatus == null }
|
||||||
.map { conversation ->
|
.map { conversation ->
|
||||||
|
ConversationEntity.from(
|
||||||
val expanded = activeAccount.alwaysOpenSpoiler
|
conversation,
|
||||||
val contentShowing = activeAccount.alwaysShowSensitiveMedia || !conversation.lastStatus!!.sensitive
|
|
||||||
val contentCollapsed = true
|
|
||||||
|
|
||||||
conversation.toEntity(
|
|
||||||
accountId = activeAccount.id,
|
accountId = activeAccount.id,
|
||||||
order = order++,
|
order = order++,
|
||||||
expanded = expanded,
|
expanded = activeAccount.alwaysOpenSpoiler,
|
||||||
contentShowing = contentShowing,
|
contentShowing = activeAccount.alwaysShowSensitiveMedia || !conversation.lastStatus!!.sensitive,
|
||||||
contentCollapsed = contentCollapsed,
|
contentCollapsed = true,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -55,14 +55,14 @@ class ConversationsViewModel @Inject constructor(
|
||||||
)
|
)
|
||||||
.flow
|
.flow
|
||||||
.map { pagingData ->
|
.map { pagingData ->
|
||||||
pagingData.map { conversation -> conversation.toViewData() }
|
pagingData.map { conversation -> ConversationViewData.from(conversation) }
|
||||||
}
|
}
|
||||||
.cachedIn(viewModelScope)
|
.cachedIn(viewModelScope)
|
||||||
|
|
||||||
fun favourite(favourite: Boolean, conversation: ConversationViewData) {
|
fun favourite(favourite: Boolean, conversation: ConversationViewData) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
timelineCases.favourite(conversation.lastStatus.id, favourite).fold({
|
timelineCases.favourite(conversation.lastStatus.id, favourite).fold({
|
||||||
val newConversation = conversation.toEntity(
|
val newConversation = conversation.toConversationEntity(
|
||||||
accountId = accountManager.activeAccount!!.id,
|
accountId = accountManager.activeAccount!!.id,
|
||||||
favourited = favourite,
|
favourited = favourite,
|
||||||
)
|
)
|
||||||
|
@ -77,7 +77,7 @@ class ConversationsViewModel @Inject constructor(
|
||||||
fun bookmark(bookmark: Boolean, conversation: ConversationViewData) {
|
fun bookmark(bookmark: Boolean, conversation: ConversationViewData) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
timelineCases.bookmark(conversation.lastStatus.id, bookmark).fold({
|
timelineCases.bookmark(conversation.lastStatus.id, bookmark).fold({
|
||||||
val newConversation = conversation.toEntity(
|
val newConversation = conversation.toConversationEntity(
|
||||||
accountId = accountManager.activeAccount!!.id,
|
accountId = accountManager.activeAccount!!.id,
|
||||||
bookmarked = bookmark,
|
bookmarked = bookmark,
|
||||||
)
|
)
|
||||||
|
@ -93,7 +93,7 @@ class ConversationsViewModel @Inject constructor(
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
timelineCases.voteInPoll(conversation.lastStatus.id, conversation.lastStatus.status.poll?.id!!, choices)
|
timelineCases.voteInPoll(conversation.lastStatus.id, conversation.lastStatus.status.poll?.id!!, choices)
|
||||||
.fold({ poll ->
|
.fold({ poll ->
|
||||||
val newConversation = conversation.toEntity(
|
val newConversation = conversation.toConversationEntity(
|
||||||
accountId = accountManager.activeAccount!!.id,
|
accountId = accountManager.activeAccount!!.id,
|
||||||
poll = poll,
|
poll = poll,
|
||||||
)
|
)
|
||||||
|
@ -107,7 +107,7 @@ class ConversationsViewModel @Inject constructor(
|
||||||
|
|
||||||
fun expandHiddenStatus(expanded: Boolean, conversation: ConversationViewData) {
|
fun expandHiddenStatus(expanded: Boolean, conversation: ConversationViewData) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val newConversation = conversation.toEntity(
|
val newConversation = conversation.toConversationEntity(
|
||||||
accountId = accountManager.activeAccount!!.id,
|
accountId = accountManager.activeAccount!!.id,
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
)
|
)
|
||||||
|
@ -117,7 +117,7 @@ class ConversationsViewModel @Inject constructor(
|
||||||
|
|
||||||
fun collapseLongStatus(collapsed: Boolean, conversation: ConversationViewData) {
|
fun collapseLongStatus(collapsed: Boolean, conversation: ConversationViewData) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val newConversation = conversation.toEntity(
|
val newConversation = conversation.toConversationEntity(
|
||||||
accountId = accountManager.activeAccount!!.id,
|
accountId = accountManager.activeAccount!!.id,
|
||||||
collapsed = collapsed,
|
collapsed = collapsed,
|
||||||
)
|
)
|
||||||
|
@ -127,7 +127,7 @@ class ConversationsViewModel @Inject constructor(
|
||||||
|
|
||||||
fun showContent(showing: Boolean, conversation: ConversationViewData) {
|
fun showContent(showing: Boolean, conversation: ConversationViewData) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val newConversation = conversation.toEntity(
|
val newConversation = conversation.toConversationEntity(
|
||||||
accountId = accountManager.activeAccount!!.id,
|
accountId = accountManager.activeAccount!!.id,
|
||||||
showingHiddenContent = showing,
|
showingHiddenContent = showing,
|
||||||
)
|
)
|
||||||
|
@ -158,7 +158,7 @@ class ConversationsViewModel @Inject constructor(
|
||||||
!(conversation.lastStatus.status.muted ?: false),
|
!(conversation.lastStatus.status.muted ?: false),
|
||||||
)
|
)
|
||||||
|
|
||||||
val newConversation = conversation.toEntity(
|
val newConversation = conversation.toConversationEntity(
|
||||||
accountId = accountManager.activeAccount!!.id,
|
accountId = accountManager.activeAccount!!.id,
|
||||||
muted = !(conversation.lastStatus.status.muted ?: false),
|
muted = !(conversation.lastStatus.status.muted ?: false),
|
||||||
)
|
)
|
||||||
|
|
|
@ -47,7 +47,6 @@ import app.pachli.util.StatusDisplayOptions
|
||||||
import app.pachli.util.deserialize
|
import app.pachli.util.deserialize
|
||||||
import app.pachli.util.serialize
|
import app.pachli.util.serialize
|
||||||
import app.pachli.util.throttleFirst
|
import app.pachli.util.throttleFirst
|
||||||
import app.pachli.util.toViewData
|
|
||||||
import app.pachli.viewdata.NotificationViewData
|
import app.pachli.viewdata.NotificationViewData
|
||||||
import app.pachli.viewdata.StatusViewData
|
import app.pachli.viewdata.StatusViewData
|
||||||
import at.connyduck.calladapter.networkresult.getOrThrow
|
import at.connyduck.calladapter.networkresult.getOrThrow
|
||||||
|
@ -534,7 +533,8 @@ class NotificationsViewModel @Inject constructor(
|
||||||
.map { pagingData ->
|
.map { pagingData ->
|
||||||
pagingData.map { notification ->
|
pagingData.map { notification ->
|
||||||
val filterAction = notification.status?.actionableStatus?.let { filterModel.shouldFilterStatus(it) } ?: Filter.Action.NONE
|
val filterAction = notification.status?.actionableStatus?.let { filterModel.shouldFilterStatus(it) } ?: Filter.Action.NONE
|
||||||
notification.toViewData(
|
NotificationViewData.from(
|
||||||
|
notification,
|
||||||
isShowingContent = statusDisplayOptions.value.showSensitiveMedia ||
|
isShowingContent = statusDisplayOptions.value.showSensitiveMedia ||
|
||||||
!(notification.status?.actionableStatus?.sensitive ?: false),
|
!(notification.status?.actionableStatus?.sensitive ?: false),
|
||||||
isExpanded = statusDisplayOptions.value.openSpoiler,
|
isExpanded = statusDisplayOptions.value.openSpoiler,
|
||||||
|
|
|
@ -35,7 +35,7 @@ import app.pachli.util.Error
|
||||||
import app.pachli.util.Loading
|
import app.pachli.util.Loading
|
||||||
import app.pachli.util.Resource
|
import app.pachli.util.Resource
|
||||||
import app.pachli.util.Success
|
import app.pachli.util.Success
|
||||||
import app.pachli.util.toViewData
|
import app.pachli.viewdata.StatusViewData
|
||||||
import at.connyduck.calladapter.networkresult.fold
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
@ -79,7 +79,7 @@ class ReportViewModel @Inject constructor(
|
||||||
.map { pagingData ->
|
.map { pagingData ->
|
||||||
/* TODO: refactor reports to use the isShowingContent / isExpanded / isCollapsed attributes from StatusViewData
|
/* TODO: refactor reports to use the isShowingContent / isExpanded / isCollapsed attributes from StatusViewData
|
||||||
instead of StatusViewState */
|
instead of StatusViewState */
|
||||||
pagingData.map { status -> status.toViewData(false, false, false) }
|
pagingData.map { status -> StatusViewData.from(status, false, false, false) }
|
||||||
}
|
}
|
||||||
.cachedIn(viewModelScope)
|
.cachedIn(viewModelScope)
|
||||||
|
|
||||||
|
|
|
@ -38,8 +38,8 @@ import app.pachli.util.setClickableMentions
|
||||||
import app.pachli.util.setClickableText
|
import app.pachli.util.setClickableText
|
||||||
import app.pachli.util.shouldTrimStatus
|
import app.pachli.util.shouldTrimStatus
|
||||||
import app.pachli.util.show
|
import app.pachli.util.show
|
||||||
|
import app.pachli.viewdata.PollViewData
|
||||||
import app.pachli.viewdata.StatusViewData
|
import app.pachli.viewdata.StatusViewData
|
||||||
import app.pachli.viewdata.toViewData
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
class StatusViewHolder(
|
class StatusViewHolder(
|
||||||
|
@ -93,7 +93,10 @@ class StatusViewHolder(
|
||||||
mediaViewHeight,
|
mediaViewHeight,
|
||||||
)
|
)
|
||||||
|
|
||||||
statusViewHelper.setupPollReadonly(viewData.status.poll.toViewData(), viewData.status.emojis, statusDisplayOptions)
|
viewData.status.poll?.let {
|
||||||
|
statusViewHelper.setupPollReadonly(PollViewData.from(it), viewData.status.emojis, statusDisplayOptions)
|
||||||
|
} ?: statusViewHelper.hidePoll()
|
||||||
|
|
||||||
setCreatedAt(viewData.status.createdAt)
|
setCreatedAt(viewData.status.createdAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,6 @@ import app.pachli.entity.DeletedStatus
|
||||||
import app.pachli.entity.Status
|
import app.pachli.entity.Status
|
||||||
import app.pachli.network.MastodonApi
|
import app.pachli.network.MastodonApi
|
||||||
import app.pachli.usecase.TimelineCases
|
import app.pachli.usecase.TimelineCases
|
||||||
import app.pachli.util.toViewData
|
|
||||||
import app.pachli.viewdata.StatusViewData
|
import app.pachli.viewdata.StatusViewData
|
||||||
import at.connyduck.calladapter.networkresult.NetworkResult
|
import at.connyduck.calladapter.networkresult.NetworkResult
|
||||||
import at.connyduck.calladapter.networkresult.fold
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
|
@ -58,7 +57,8 @@ class SearchViewModel @Inject constructor(
|
||||||
|
|
||||||
private val statusesPagingSourceFactory = SearchPagingSourceFactory(mastodonApi, SearchType.Status, loadedStatuses) {
|
private val statusesPagingSourceFactory = SearchPagingSourceFactory(mastodonApi, SearchType.Status, loadedStatuses) {
|
||||||
it.statuses.map { status ->
|
it.statuses.map { status ->
|
||||||
status.toViewData(
|
StatusViewData.from(
|
||||||
|
status,
|
||||||
isShowingContent = alwaysShowSensitiveMedia || !status.actionableStatus.sensitive,
|
isShowingContent = alwaysShowSensitiveMedia || !status.actionableStatus.sensitive,
|
||||||
isExpanded = alwaysOpenSpoiler,
|
isExpanded = alwaysOpenSpoiler,
|
||||||
isCollapsed = true,
|
isCollapsed = true,
|
||||||
|
|
|
@ -1,235 +0,0 @@
|
||||||
/* Copyright 2021 Tusky Contributors
|
|
||||||
*
|
|
||||||
* This file is a part of Pachli.
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*
|
|
||||||
* Pachli 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 Tusky; if not,
|
|
||||||
* see <http://www.gnu.org/licenses>. */
|
|
||||||
|
|
||||||
package app.pachli.components.timeline
|
|
||||||
|
|
||||||
import app.pachli.db.TimelineAccountEntity
|
|
||||||
import app.pachli.db.TimelineStatusEntity
|
|
||||||
import app.pachli.db.TimelineStatusWithAccount
|
|
||||||
import app.pachli.entity.Attachment
|
|
||||||
import app.pachli.entity.Card
|
|
||||||
import app.pachli.entity.Emoji
|
|
||||||
import app.pachli.entity.HashTag
|
|
||||||
import app.pachli.entity.Poll
|
|
||||||
import app.pachli.entity.Status
|
|
||||||
import app.pachli.entity.TimelineAccount
|
|
||||||
import app.pachli.viewdata.StatusViewData
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
@Suppress("unused")
|
|
||||||
private const val TAG = "TimelineTypeMappers"
|
|
||||||
|
|
||||||
private val attachmentArrayListType = object : TypeToken<ArrayList<Attachment>>() {}.type
|
|
||||||
private val emojisListType = object : TypeToken<List<Emoji>>() {}.type
|
|
||||||
private val mentionListType = object : TypeToken<List<Status.Mention>>() {}.type
|
|
||||||
private val tagListType = object : TypeToken<List<HashTag>>() {}.type
|
|
||||||
|
|
||||||
fun TimelineAccount.toEntity(accountId: Long, gson: Gson): TimelineAccountEntity {
|
|
||||||
return TimelineAccountEntity(
|
|
||||||
serverId = id,
|
|
||||||
timelineUserId = accountId,
|
|
||||||
localUsername = localUsername,
|
|
||||||
username = username,
|
|
||||||
displayName = name,
|
|
||||||
url = url,
|
|
||||||
avatar = avatar,
|
|
||||||
emojis = gson.toJson(emojis),
|
|
||||||
bot = bot,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun TimelineAccountEntity.toAccount(gson: Gson): TimelineAccount {
|
|
||||||
return TimelineAccount(
|
|
||||||
id = serverId,
|
|
||||||
localUsername = localUsername,
|
|
||||||
username = username,
|
|
||||||
displayName = displayName,
|
|
||||||
note = "",
|
|
||||||
url = url,
|
|
||||||
avatar = avatar,
|
|
||||||
bot = bot,
|
|
||||||
emojis = gson.fromJson(emojis, emojisListType),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Status.toEntity(
|
|
||||||
timelineUserId: Long,
|
|
||||||
gson: Gson,
|
|
||||||
): TimelineStatusEntity {
|
|
||||||
return TimelineStatusEntity(
|
|
||||||
serverId = this.id,
|
|
||||||
url = actionableStatus.url,
|
|
||||||
timelineUserId = timelineUserId,
|
|
||||||
authorServerId = actionableStatus.account.id,
|
|
||||||
inReplyToId = actionableStatus.inReplyToId,
|
|
||||||
inReplyToAccountId = actionableStatus.inReplyToAccountId,
|
|
||||||
content = actionableStatus.content,
|
|
||||||
createdAt = actionableStatus.createdAt.time,
|
|
||||||
editedAt = actionableStatus.editedAt?.time,
|
|
||||||
emojis = actionableStatus.emojis.let(gson::toJson),
|
|
||||||
reblogsCount = actionableStatus.reblogsCount,
|
|
||||||
favouritesCount = actionableStatus.favouritesCount,
|
|
||||||
reblogged = actionableStatus.reblogged,
|
|
||||||
favourited = actionableStatus.favourited,
|
|
||||||
bookmarked = actionableStatus.bookmarked,
|
|
||||||
sensitive = actionableStatus.sensitive,
|
|
||||||
spoilerText = actionableStatus.spoilerText,
|
|
||||||
visibility = actionableStatus.visibility,
|
|
||||||
attachments = actionableStatus.attachments.let(gson::toJson),
|
|
||||||
mentions = actionableStatus.mentions.let(gson::toJson),
|
|
||||||
tags = actionableStatus.tags.let(gson::toJson),
|
|
||||||
application = actionableStatus.application.let(gson::toJson),
|
|
||||||
reblogServerId = reblog?.id,
|
|
||||||
reblogAccountId = reblog?.let { this.account.id },
|
|
||||||
poll = actionableStatus.poll.let(gson::toJson),
|
|
||||||
muted = actionableStatus.muted,
|
|
||||||
pinned = actionableStatus.pinned == true,
|
|
||||||
card = actionableStatus.card?.let(gson::toJson),
|
|
||||||
repliesCount = actionableStatus.repliesCount,
|
|
||||||
language = actionableStatus.language,
|
|
||||||
filtered = actionableStatus.filtered,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun TimelineStatusWithAccount.toViewData(gson: Gson, alwaysOpenSpoiler: Boolean, alwaysShowSensitiveMedia: Boolean, isDetailed: Boolean = false): StatusViewData {
|
|
||||||
val attachments: ArrayList<Attachment> = gson.fromJson(
|
|
||||||
status.attachments,
|
|
||||||
attachmentArrayListType,
|
|
||||||
) ?: arrayListOf()
|
|
||||||
val mentions: List<Status.Mention> = gson.fromJson(
|
|
||||||
status.mentions,
|
|
||||||
mentionListType,
|
|
||||||
) ?: emptyList()
|
|
||||||
val tags: List<HashTag>? = gson.fromJson(
|
|
||||||
status.tags,
|
|
||||||
tagListType,
|
|
||||||
)
|
|
||||||
val application = gson.fromJson(status.application, Status.Application::class.java)
|
|
||||||
val emojis: List<Emoji> = gson.fromJson(
|
|
||||||
status.emojis,
|
|
||||||
emojisListType,
|
|
||||||
) ?: emptyList()
|
|
||||||
val poll: Poll? = gson.fromJson(status.poll, Poll::class.java)
|
|
||||||
val card: Card? = gson.fromJson(status.card, Card::class.java)
|
|
||||||
|
|
||||||
val reblog = status.reblogServerId?.let { id ->
|
|
||||||
Status(
|
|
||||||
id = id,
|
|
||||||
url = status.url,
|
|
||||||
account = account.toAccount(gson),
|
|
||||||
inReplyToId = status.inReplyToId,
|
|
||||||
inReplyToAccountId = status.inReplyToAccountId,
|
|
||||||
reblog = null,
|
|
||||||
content = status.content.orEmpty(),
|
|
||||||
createdAt = Date(status.createdAt),
|
|
||||||
editedAt = status.editedAt?.let { Date(it) },
|
|
||||||
emojis = emojis,
|
|
||||||
reblogsCount = status.reblogsCount,
|
|
||||||
favouritesCount = status.favouritesCount,
|
|
||||||
reblogged = status.reblogged,
|
|
||||||
favourited = status.favourited,
|
|
||||||
bookmarked = status.bookmarked,
|
|
||||||
sensitive = status.sensitive,
|
|
||||||
spoilerText = status.spoilerText,
|
|
||||||
visibility = status.visibility,
|
|
||||||
attachments = attachments,
|
|
||||||
mentions = mentions,
|
|
||||||
tags = tags,
|
|
||||||
application = application,
|
|
||||||
pinned = false,
|
|
||||||
muted = status.muted,
|
|
||||||
poll = poll,
|
|
||||||
card = card,
|
|
||||||
repliesCount = status.repliesCount,
|
|
||||||
language = status.language,
|
|
||||||
filtered = status.filtered,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val status = if (reblog != null) {
|
|
||||||
Status(
|
|
||||||
id = status.serverId,
|
|
||||||
url = null, // no url for reblogs
|
|
||||||
account = this.reblogAccount!!.toAccount(gson),
|
|
||||||
inReplyToId = null,
|
|
||||||
inReplyToAccountId = null,
|
|
||||||
reblog = reblog,
|
|
||||||
content = "",
|
|
||||||
createdAt = Date(status.createdAt), // lie but whatever?
|
|
||||||
editedAt = null,
|
|
||||||
emojis = listOf(),
|
|
||||||
reblogsCount = 0,
|
|
||||||
favouritesCount = 0,
|
|
||||||
reblogged = false,
|
|
||||||
favourited = false,
|
|
||||||
bookmarked = false,
|
|
||||||
sensitive = false,
|
|
||||||
spoilerText = "",
|
|
||||||
visibility = status.visibility,
|
|
||||||
attachments = ArrayList(),
|
|
||||||
mentions = listOf(),
|
|
||||||
tags = listOf(),
|
|
||||||
application = null,
|
|
||||||
pinned = status.pinned,
|
|
||||||
muted = status.muted,
|
|
||||||
poll = null,
|
|
||||||
card = null,
|
|
||||||
repliesCount = status.repliesCount,
|
|
||||||
language = status.language,
|
|
||||||
filtered = status.filtered,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Status(
|
|
||||||
id = status.serverId,
|
|
||||||
url = status.url,
|
|
||||||
account = account.toAccount(gson),
|
|
||||||
inReplyToId = status.inReplyToId,
|
|
||||||
inReplyToAccountId = status.inReplyToAccountId,
|
|
||||||
reblog = null,
|
|
||||||
content = status.content.orEmpty(),
|
|
||||||
createdAt = Date(status.createdAt),
|
|
||||||
editedAt = status.editedAt?.let { Date(it) },
|
|
||||||
emojis = emojis,
|
|
||||||
reblogsCount = status.reblogsCount,
|
|
||||||
favouritesCount = status.favouritesCount,
|
|
||||||
reblogged = status.reblogged,
|
|
||||||
favourited = status.favourited,
|
|
||||||
bookmarked = status.bookmarked,
|
|
||||||
sensitive = status.sensitive,
|
|
||||||
spoilerText = status.spoilerText,
|
|
||||||
visibility = status.visibility,
|
|
||||||
attachments = attachments,
|
|
||||||
mentions = mentions,
|
|
||||||
tags = tags,
|
|
||||||
application = application,
|
|
||||||
pinned = status.pinned,
|
|
||||||
muted = status.muted,
|
|
||||||
poll = poll,
|
|
||||||
card = card,
|
|
||||||
repliesCount = status.repliesCount,
|
|
||||||
language = status.language,
|
|
||||||
filtered = status.filtered,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return StatusViewData(
|
|
||||||
status = status,
|
|
||||||
isExpanded = this.viewData?.expanded ?: alwaysOpenSpoiler,
|
|
||||||
isShowingContent = this.viewData?.contentShowing ?: (alwaysShowSensitiveMedia || !status.actionableStatus.sensitive),
|
|
||||||
isCollapsed = this.viewData?.contentCollapsed ?: true,
|
|
||||||
isDetailed = isDetailed,
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -26,11 +26,12 @@ import androidx.paging.PagingState
|
||||||
import androidx.paging.RemoteMediator
|
import androidx.paging.RemoteMediator
|
||||||
import androidx.room.Transaction
|
import androidx.room.Transaction
|
||||||
import androidx.room.withTransaction
|
import androidx.room.withTransaction
|
||||||
import app.pachli.components.timeline.toEntity
|
|
||||||
import app.pachli.db.AccountManager
|
import app.pachli.db.AccountManager
|
||||||
import app.pachli.db.AppDatabase
|
import app.pachli.db.AppDatabase
|
||||||
import app.pachli.db.RemoteKeyEntity
|
import app.pachli.db.RemoteKeyEntity
|
||||||
import app.pachli.db.RemoteKeyKind
|
import app.pachli.db.RemoteKeyKind
|
||||||
|
import app.pachli.db.TimelineAccountEntity
|
||||||
|
import app.pachli.db.TimelineStatusEntity
|
||||||
import app.pachli.db.TimelineStatusWithAccount
|
import app.pachli.db.TimelineStatusWithAccount
|
||||||
import app.pachli.entity.Status
|
import app.pachli.entity.Status
|
||||||
import app.pachli.network.Links
|
import app.pachli.network.Links
|
||||||
|
@ -258,13 +259,15 @@ class CachedTimelineRemoteMediator(
|
||||||
@Transaction
|
@Transaction
|
||||||
private suspend fun insertStatuses(statuses: List<Status>) {
|
private suspend fun insertStatuses(statuses: List<Status>) {
|
||||||
for (status in statuses) {
|
for (status in statuses) {
|
||||||
timelineDao.insertAccount(status.account.toEntity(activeAccount.id, gson))
|
timelineDao.insertAccount(TimelineAccountEntity.from(status.account, activeAccount.id, gson))
|
||||||
status.reblog?.account?.toEntity(activeAccount.id, gson)?.let { rebloggedAccount ->
|
status.reblog?.account?.let {
|
||||||
timelineDao.insertAccount(rebloggedAccount)
|
val account = TimelineAccountEntity.from(it, activeAccount.id, gson)
|
||||||
|
timelineDao.insertAccount(account)
|
||||||
}
|
}
|
||||||
|
|
||||||
timelineDao.insertStatus(
|
timelineDao.insertStatus(
|
||||||
status.toEntity(
|
TimelineStatusEntity.from(
|
||||||
|
status,
|
||||||
timelineUserId = activeAccount.id,
|
timelineUserId = activeAccount.id,
|
||||||
gson = gson,
|
gson = gson,
|
||||||
),
|
),
|
||||||
|
|
|
@ -32,7 +32,6 @@ import app.pachli.appstore.ReblogEvent
|
||||||
import app.pachli.components.timeline.CachedTimelineRepository
|
import app.pachli.components.timeline.CachedTimelineRepository
|
||||||
import app.pachli.components.timeline.FiltersRepository
|
import app.pachli.components.timeline.FiltersRepository
|
||||||
import app.pachli.components.timeline.TimelineKind
|
import app.pachli.components.timeline.TimelineKind
|
||||||
import app.pachli.components.timeline.toViewData
|
|
||||||
import app.pachli.db.AccountManager
|
import app.pachli.db.AccountManager
|
||||||
import app.pachli.entity.Filter
|
import app.pachli.entity.Filter
|
||||||
import app.pachli.entity.Poll
|
import app.pachli.entity.Poll
|
||||||
|
@ -97,10 +96,11 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
.map { pagingData ->
|
.map { pagingData ->
|
||||||
pagingData
|
pagingData
|
||||||
.map {
|
.map {
|
||||||
it.toViewData(
|
StatusViewData.from(
|
||||||
|
it,
|
||||||
gson,
|
gson,
|
||||||
alwaysOpenSpoiler = activeAccount.alwaysOpenSpoiler,
|
isExpanded = activeAccount.alwaysOpenSpoiler,
|
||||||
alwaysShowSensitiveMedia = activeAccount.alwaysShowSensitiveMedia,
|
isShowingContent = activeAccount.alwaysShowSensitiveMedia,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.filter { shouldFilterStatus(it) != Filter.Action.HIDE }
|
.filter { shouldFilterStatus(it) != Filter.Action.HIDE }
|
||||||
|
|
|
@ -38,7 +38,6 @@ import app.pachli.entity.Poll
|
||||||
import app.pachli.network.FilterModel
|
import app.pachli.network.FilterModel
|
||||||
import app.pachli.settings.AccountPreferenceDataStore
|
import app.pachli.settings.AccountPreferenceDataStore
|
||||||
import app.pachli.usecase.TimelineCases
|
import app.pachli.usecase.TimelineCases
|
||||||
import app.pachli.util.toViewData
|
|
||||||
import app.pachli.viewdata.StatusViewData
|
import app.pachli.viewdata.StatusViewData
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
@ -90,7 +89,8 @@ class NetworkTimelineViewModel @Inject constructor(
|
||||||
return repository.getStatusStream(viewModelScope, kind = kind, initialKey = initialKey)
|
return repository.getStatusStream(viewModelScope, kind = kind, initialKey = initialKey)
|
||||||
.map { pagingData ->
|
.map { pagingData ->
|
||||||
pagingData.map {
|
pagingData.map {
|
||||||
modifiedViewData[it.id] ?: it.toViewData(
|
modifiedViewData[it.id] ?: StatusViewData.from(
|
||||||
|
it,
|
||||||
isShowingContent = statusDisplayOptions.value.showSensitiveMedia || !it.actionableStatus.sensitive,
|
isShowingContent = statusDisplayOptions.value.showSensitiveMedia || !it.actionableStatus.sensitive,
|
||||||
isExpanded = statusDisplayOptions.value.openSpoiler,
|
isExpanded = statusDisplayOptions.value.openSpoiler,
|
||||||
isCollapsed = true,
|
isCollapsed = true,
|
||||||
|
|
|
@ -21,10 +21,10 @@ import androidx.lifecycle.viewModelScope
|
||||||
import app.pachli.appstore.EventHub
|
import app.pachli.appstore.EventHub
|
||||||
import app.pachli.appstore.PreferenceChangedEvent
|
import app.pachli.appstore.PreferenceChangedEvent
|
||||||
import app.pachli.entity.Filter
|
import app.pachli.entity.Filter
|
||||||
|
import app.pachli.entity.TrendingTag
|
||||||
import app.pachli.entity.end
|
import app.pachli.entity.end
|
||||||
import app.pachli.entity.start
|
import app.pachli.entity.start
|
||||||
import app.pachli.network.MastodonApi
|
import app.pachli.network.MastodonApi
|
||||||
import app.pachli.util.toViewData
|
|
||||||
import app.pachli.viewdata.TrendingViewData
|
import app.pachli.viewdata.TrendingViewData
|
||||||
import at.connyduck.calladapter.networkresult.fold
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
|
@ -97,7 +97,7 @@ class TrendingTagsViewModel @Inject constructor(
|
||||||
} ?: false
|
} ?: false
|
||||||
}
|
}
|
||||||
.sortedByDescending { tag -> tag.history.sumOf { it.uses.toLongOrNull() ?: 0 } }
|
.sortedByDescending { tag -> tag.history.sumOf { it.uses.toLongOrNull() ?: 0 } }
|
||||||
.toViewData()
|
.toTrendingViewDataTag()
|
||||||
|
|
||||||
val header = TrendingViewData.Header(firstTag.start(), firstTag.end())
|
val header = TrendingViewData.Header(firstTag.start(), firstTag.end())
|
||||||
TrendingTagsUiState(listOf(header) + tags, LoadingState.LOADED)
|
TrendingTagsUiState(listOf(header) + tags, LoadingState.LOADED)
|
||||||
|
@ -114,6 +114,14 @@ class TrendingTagsViewModel @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun List<TrendingTag>.toTrendingViewDataTag(): List<TrendingViewData.Tag> {
|
||||||
|
val maxTrendingValue = flatMap { tag -> tag.history }
|
||||||
|
.mapNotNull { it.uses.toLongOrNull() }
|
||||||
|
.maxOrNull() ?: 1
|
||||||
|
|
||||||
|
return map { TrendingViewData.Tag.from(it, maxTrendingValue) }
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "TrendingViewModel"
|
private const val TAG = "TrendingViewModel"
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,19 +28,16 @@ import app.pachli.appstore.StatusComposedEvent
|
||||||
import app.pachli.appstore.StatusDeletedEvent
|
import app.pachli.appstore.StatusDeletedEvent
|
||||||
import app.pachli.appstore.StatusEditedEvent
|
import app.pachli.appstore.StatusEditedEvent
|
||||||
import app.pachli.components.timeline.CachedTimelineRepository
|
import app.pachli.components.timeline.CachedTimelineRepository
|
||||||
import app.pachli.components.timeline.toViewData
|
|
||||||
import app.pachli.components.timeline.util.ifExpected
|
import app.pachli.components.timeline.util.ifExpected
|
||||||
import app.pachli.db.AccountEntity
|
import app.pachli.db.AccountEntity
|
||||||
import app.pachli.db.AccountManager
|
import app.pachli.db.AccountManager
|
||||||
import app.pachli.db.AppDatabase
|
import app.pachli.db.AppDatabase
|
||||||
import app.pachli.db.StatusViewDataEntity
|
|
||||||
import app.pachli.entity.Filter
|
import app.pachli.entity.Filter
|
||||||
import app.pachli.entity.FilterV1
|
import app.pachli.entity.FilterV1
|
||||||
import app.pachli.entity.Status
|
import app.pachli.entity.Status
|
||||||
import app.pachli.network.FilterModel
|
import app.pachli.network.FilterModel
|
||||||
import app.pachli.network.MastodonApi
|
import app.pachli.network.MastodonApi
|
||||||
import app.pachli.usecase.TimelineCases
|
import app.pachli.usecase.TimelineCases
|
||||||
import app.pachli.util.toViewData
|
|
||||||
import app.pachli.viewdata.StatusViewData
|
import app.pachli.viewdata.StatusViewData
|
||||||
import at.connyduck.calladapter.networkresult.fold
|
import at.connyduck.calladapter.networkresult.fold
|
||||||
import at.connyduck.calladapter.networkresult.getOrElse
|
import at.connyduck.calladapter.networkresult.getOrElse
|
||||||
|
@ -113,25 +110,32 @@ class ViewThreadViewModel @Inject constructor(
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
Log.d(TAG, "Finding status with: $id")
|
Log.d(TAG, "Finding status with: $id")
|
||||||
val contextCall = async { api.statusContext(id) }
|
val contextCall = async { api.statusContext(id) }
|
||||||
val timelineStatus = db.timelineDao().getStatus(id)
|
val timelineStatusWithAccount = db.timelineDao().getStatus(id)
|
||||||
|
|
||||||
var detailedStatus = if (timelineStatus != null) {
|
var detailedStatus = if (timelineStatusWithAccount != null) {
|
||||||
Log.d(TAG, "Loaded status from local timeline")
|
Log.d(TAG, "Loaded status from local timeline")
|
||||||
val viewData = timelineStatus.toViewData(
|
val status = timelineStatusWithAccount.toStatus(gson)
|
||||||
gson,
|
|
||||||
alwaysOpenSpoiler = alwaysOpenSpoiler,
|
|
||||||
alwaysShowSensitiveMedia = alwaysShowSensitiveMedia,
|
|
||||||
isDetailed = true,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Return the correct status, depending on which one matched. If you do not do
|
// Return the correct status, depending on which one matched. If you do not do
|
||||||
// this the status IDs will be different between the status that's displayed with
|
// this the status IDs will be different between the status that's displayed with
|
||||||
// ThreadUiState.LoadingThread and ThreadUiState.Success, even though the apparent
|
// ThreadUiState.LoadingThread and ThreadUiState.Success, even though the apparent
|
||||||
// status content is the same. Then the status flickers as it is drawn twice.
|
// status content is the same. Then the status flickers as it is drawn twice.
|
||||||
if (viewData.actionableId == id) {
|
if (status.actionableId == id) {
|
||||||
viewData.actionable.toViewData(isDetailed = true, viewData)
|
StatusViewData.from(
|
||||||
|
status = status.actionableStatus,
|
||||||
|
isExpanded = timelineStatusWithAccount.viewData?.expanded ?: alwaysOpenSpoiler,
|
||||||
|
isShowingContent = timelineStatusWithAccount.viewData?.contentShowing ?: alwaysShowSensitiveMedia,
|
||||||
|
isCollapsed = timelineStatusWithAccount.viewData?.contentCollapsed ?: true,
|
||||||
|
isDetailed = true,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
viewData
|
StatusViewData.from(
|
||||||
|
timelineStatusWithAccount,
|
||||||
|
gson,
|
||||||
|
isExpanded = alwaysOpenSpoiler,
|
||||||
|
isShowingContent = alwaysShowSensitiveMedia,
|
||||||
|
isDetailed = true,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "Loaded status from network")
|
Log.d(TAG, "Loaded status from network")
|
||||||
|
@ -139,7 +143,7 @@ class ViewThreadViewModel @Inject constructor(
|
||||||
_uiState.value = ThreadUiState.Error(exception)
|
_uiState.value = ThreadUiState.Error(exception)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
result.toViewData(isDetailed = true)
|
StatusViewData.fromStatusAndUiState(result, isDetailed = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
_uiState.value = ThreadUiState.LoadingThread(
|
_uiState.value = ThreadUiState.LoadingThread(
|
||||||
|
@ -151,9 +155,16 @@ class ViewThreadViewModel @Inject constructor(
|
||||||
// compared to the remote one. Now the user has a working UI do a background fetch
|
// compared to the remote one. Now the user has a working UI do a background fetch
|
||||||
// for the status. Ignore errors, the user still has a functioning UI if the fetch
|
// for the status. Ignore errors, the user still has a functioning UI if the fetch
|
||||||
// failed.
|
// failed.
|
||||||
if (timelineStatus != null) {
|
if (timelineStatusWithAccount != null) {
|
||||||
val viewData = api.status(id).getOrNull()?.toViewData(isDetailed = true, detailedStatus)
|
api.status(id).getOrNull()?.let {
|
||||||
if (viewData != null) { detailedStatus = viewData }
|
detailedStatus = StatusViewData.from(
|
||||||
|
it,
|
||||||
|
isShowingContent = detailedStatus.isShowingContent,
|
||||||
|
isExpanded = detailedStatus.isExpanded,
|
||||||
|
isCollapsed = detailedStatus.isCollapsed,
|
||||||
|
isDetailed = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val contextResult = contextCall.await()
|
val contextResult = contextCall.await()
|
||||||
|
@ -163,12 +174,26 @@ class ViewThreadViewModel @Inject constructor(
|
||||||
val cachedViewData = repository.getStatusViewData(ids)
|
val cachedViewData = repository.getStatusViewData(ids)
|
||||||
val ancestors = statusContext.ancestors.map {
|
val ancestors = statusContext.ancestors.map {
|
||||||
status ->
|
status ->
|
||||||
status.toViewData(statusViewDataEntity = cachedViewData[status.id])
|
val svd = cachedViewData[status.id]
|
||||||
}.filter()
|
StatusViewData.from(
|
||||||
|
status,
|
||||||
|
isShowingContent = svd?.contentShowing ?: (alwaysShowSensitiveMedia || !status.actionableStatus.sensitive),
|
||||||
|
isExpanded = svd?.expanded ?: alwaysOpenSpoiler,
|
||||||
|
isCollapsed = svd?.contentCollapsed ?: true,
|
||||||
|
isDetailed = false,
|
||||||
|
)
|
||||||
|
}.filterByFilterAction()
|
||||||
val descendants = statusContext.descendants.map {
|
val descendants = statusContext.descendants.map {
|
||||||
status ->
|
status ->
|
||||||
status.toViewData(statusViewDataEntity = cachedViewData[status.id])
|
val svd = cachedViewData[status.id]
|
||||||
}.filter()
|
StatusViewData.from(
|
||||||
|
status,
|
||||||
|
isShowingContent = svd?.contentShowing ?: (alwaysShowSensitiveMedia || !status.actionableStatus.sensitive),
|
||||||
|
isExpanded = svd?.expanded ?: alwaysOpenSpoiler,
|
||||||
|
isCollapsed = svd?.contentCollapsed ?: true,
|
||||||
|
isDetailed = false,
|
||||||
|
)
|
||||||
|
}.filterByFilterAction()
|
||||||
val statuses = ancestors + detailedStatus + descendants
|
val statuses = ancestors + detailedStatus + descendants
|
||||||
|
|
||||||
_uiState.value = ThreadUiState.Success(
|
_uiState.value = ThreadUiState.Success(
|
||||||
|
@ -345,7 +370,7 @@ class ViewThreadViewModel @Inject constructor(
|
||||||
if (detailedIndex != -1 && repliedIndex >= detailedIndex) {
|
if (detailedIndex != -1 && repliedIndex >= detailedIndex) {
|
||||||
// there is a new reply to the detailed status or below -> display it
|
// there is a new reply to the detailed status or below -> display it
|
||||||
val newStatuses = statuses.subList(0, repliedIndex + 1) +
|
val newStatuses = statuses.subList(0, repliedIndex + 1) +
|
||||||
eventStatus.toViewData() +
|
StatusViewData.fromStatusAndUiState(eventStatus) +
|
||||||
statuses.subList(repliedIndex + 1, statuses.size)
|
statuses.subList(repliedIndex + 1, statuses.size)
|
||||||
uiState.copy(statusViewData = newStatuses)
|
uiState.copy(statusViewData = newStatuses)
|
||||||
} else {
|
} else {
|
||||||
|
@ -359,7 +384,7 @@ class ViewThreadViewModel @Inject constructor(
|
||||||
uiState.copy(
|
uiState.copy(
|
||||||
statusViewData = uiState.statusViewData.map { status ->
|
statusViewData = uiState.statusViewData.map { status ->
|
||||||
if (status.actionableId == event.originalId) {
|
if (status.actionableId == event.originalId) {
|
||||||
event.status.toViewData()
|
StatusViewData.fromStatusAndUiState(event.status)
|
||||||
} else {
|
} else {
|
||||||
status
|
status
|
||||||
}
|
}
|
||||||
|
@ -465,7 +490,7 @@ class ViewThreadViewModel @Inject constructor(
|
||||||
|
|
||||||
private fun updateStatuses() {
|
private fun updateStatuses() {
|
||||||
updateSuccess { uiState ->
|
updateSuccess { uiState ->
|
||||||
val statuses = uiState.statusViewData.filter()
|
val statuses = uiState.statusViewData.filterByFilterAction()
|
||||||
uiState.copy(
|
uiState.copy(
|
||||||
statusViewData = statuses,
|
statusViewData = statuses,
|
||||||
revealButton = statuses.getRevealButtonState(),
|
revealButton = statuses.getRevealButtonState(),
|
||||||
|
@ -473,7 +498,7 @@ class ViewThreadViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<StatusViewData>.filter(): List<StatusViewData> {
|
private fun List<StatusViewData>.filterByFilterAction(): List<StatusViewData> {
|
||||||
return filter { status ->
|
return filter { status ->
|
||||||
if (status.isDetailed) {
|
if (status.isDetailed) {
|
||||||
true
|
true
|
||||||
|
@ -485,35 +510,14 @@ class ViewThreadViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert the status to a [StatusViewData], copying the view data from [statusViewData]
|
* Creates a [StatusViewData] from `status`, copying over the viewdata state from the same
|
||||||
|
* status in _uiState (if that status exists).
|
||||||
*/
|
*/
|
||||||
private fun Status.toViewData(isDetailed: Boolean = false, statusViewData: StatusViewData): StatusViewData {
|
private fun StatusViewData.Companion.fromStatusAndUiState(status: Status, isDetailed: Boolean = false): StatusViewData {
|
||||||
return toViewData(
|
val oldStatus = (_uiState.value as? ThreadUiState.Success)?.statusViewData?.find { it.id == status.id }
|
||||||
isShowingContent = statusViewData.isShowingContent,
|
return from(
|
||||||
isExpanded = statusViewData.isExpanded,
|
status,
|
||||||
isCollapsed = statusViewData.isCollapsed,
|
isShowingContent = oldStatus?.isShowingContent ?: (alwaysShowSensitiveMedia || !status.actionableStatus.sensitive),
|
||||||
isDetailed = isDetailed,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert the status to a [StatusViewData], copying the view data from [statusViewDataEntity]
|
|
||||||
*/
|
|
||||||
private fun Status.toViewData(isDetailed: Boolean = false, statusViewDataEntity: StatusViewDataEntity?): StatusViewData {
|
|
||||||
return toViewData(
|
|
||||||
isShowingContent = statusViewDataEntity?.contentShowing ?: (alwaysShowSensitiveMedia || !actionableStatus.sensitive),
|
|
||||||
isExpanded = statusViewDataEntity?.expanded ?: alwaysOpenSpoiler,
|
|
||||||
isCollapsed = statusViewDataEntity?.contentCollapsed ?: !isDetailed,
|
|
||||||
isDetailed = isDetailed,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Status.toViewData(
|
|
||||||
isDetailed: Boolean = false,
|
|
||||||
): StatusViewData {
|
|
||||||
val oldStatus = (_uiState.value as? ThreadUiState.Success)?.statusViewData?.find { it.id == this.id }
|
|
||||||
return toViewData(
|
|
||||||
isShowingContent = oldStatus?.isShowingContent ?: (alwaysShowSensitiveMedia || !actionableStatus.sensitive),
|
|
||||||
isExpanded = oldStatus?.isExpanded ?: alwaysOpenSpoiler,
|
isExpanded = oldStatus?.isExpanded ?: alwaysOpenSpoiler,
|
||||||
isCollapsed = oldStatus?.isCollapsed ?: !isDetailed,
|
isCollapsed = oldStatus?.isCollapsed ?: !isDetailed,
|
||||||
isDetailed = oldStatus?.isDetailed ?: isDetailed,
|
isDetailed = oldStatus?.isDetailed ?: isDetailed,
|
||||||
|
|
|
@ -35,7 +35,7 @@ import app.pachli.util.parseAsMastodonHtml
|
||||||
import app.pachli.util.setClickableText
|
import app.pachli.util.setClickableText
|
||||||
import app.pachli.util.show
|
import app.pachli.util.show
|
||||||
import app.pachli.util.visible
|
import app.pachli.util.visible
|
||||||
import app.pachli.viewdata.toViewData
|
import app.pachli.viewdata.PollOptionViewData
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.google.android.material.color.MaterialColors
|
import com.google.android.material.color.MaterialColors
|
||||||
import org.xml.sax.XMLReader
|
import org.xml.sax.XMLReader
|
||||||
|
@ -138,7 +138,7 @@ class ViewEditsAdapter(
|
||||||
binding.statusEditPollOptions.layoutManager = LinearLayoutManager(context)
|
binding.statusEditPollOptions.layoutManager = LinearLayoutManager(context)
|
||||||
|
|
||||||
pollAdapter.setup(
|
pollAdapter.setup(
|
||||||
options = edit.poll.options.map { it.toViewData(false) },
|
options = edit.poll.options.map { PollOptionViewData.from(it, false) },
|
||||||
voteCount = 0,
|
voteCount = 0,
|
||||||
votersCount = null,
|
votersCount = null,
|
||||||
emojis = edit.emojis,
|
emojis = edit.emojis,
|
||||||
|
|
|
@ -20,8 +20,18 @@ import androidx.room.Entity
|
||||||
import androidx.room.ForeignKey
|
import androidx.room.ForeignKey
|
||||||
import androidx.room.Index
|
import androidx.room.Index
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
|
import app.pachli.entity.Attachment
|
||||||
|
import app.pachli.entity.Card
|
||||||
|
import app.pachli.entity.Emoji
|
||||||
import app.pachli.entity.FilterResult
|
import app.pachli.entity.FilterResult
|
||||||
|
import app.pachli.entity.HashTag
|
||||||
|
import app.pachli.entity.Poll
|
||||||
import app.pachli.entity.Status
|
import app.pachli.entity.Status
|
||||||
|
import app.pachli.entity.TimelineAccount
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.reflect.TypeToken
|
||||||
|
import java.lang.reflect.Type
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We're trying to play smart here. Server sends us reblogs as two entities one embedded into
|
* We're trying to play smart here. Server sends us reblogs as two entities one embedded into
|
||||||
|
@ -82,7 +92,43 @@ data class TimelineStatusEntity(
|
||||||
val card: String?,
|
val card: String?,
|
||||||
val language: String?,
|
val language: String?,
|
||||||
val filtered: List<FilterResult>?,
|
val filtered: List<FilterResult>?,
|
||||||
)
|
) {
|
||||||
|
companion object {
|
||||||
|
fun from(status: Status, timelineUserId: Long, gson: Gson) = TimelineStatusEntity(
|
||||||
|
serverId = status.id,
|
||||||
|
url = status.actionableStatus.url,
|
||||||
|
timelineUserId = timelineUserId,
|
||||||
|
authorServerId = status.actionableStatus.account.id,
|
||||||
|
inReplyToId = status.actionableStatus.inReplyToId,
|
||||||
|
inReplyToAccountId = status.actionableStatus.inReplyToAccountId,
|
||||||
|
content = status.actionableStatus.content,
|
||||||
|
createdAt = status.actionableStatus.createdAt.time,
|
||||||
|
editedAt = status.actionableStatus.editedAt?.time,
|
||||||
|
emojis = status.actionableStatus.emojis.let(gson::toJson),
|
||||||
|
reblogsCount = status.actionableStatus.reblogsCount,
|
||||||
|
favouritesCount = status.actionableStatus.favouritesCount,
|
||||||
|
reblogged = status.actionableStatus.reblogged,
|
||||||
|
favourited = status.actionableStatus.favourited,
|
||||||
|
bookmarked = status.actionableStatus.bookmarked,
|
||||||
|
sensitive = status.actionableStatus.sensitive,
|
||||||
|
spoilerText = status.actionableStatus.spoilerText,
|
||||||
|
visibility = status.actionableStatus.visibility,
|
||||||
|
attachments = status.actionableStatus.attachments.let(gson::toJson),
|
||||||
|
mentions = status.actionableStatus.mentions.let(gson::toJson),
|
||||||
|
tags = status.actionableStatus.tags.let(gson::toJson),
|
||||||
|
application = status.actionableStatus.application.let(gson::toJson),
|
||||||
|
reblogServerId = status.reblog?.id,
|
||||||
|
reblogAccountId = status.reblog?.let { status.account.id },
|
||||||
|
poll = status.actionableStatus.poll.let(gson::toJson),
|
||||||
|
muted = status.actionableStatus.muted,
|
||||||
|
pinned = status.actionableStatus.pinned == true,
|
||||||
|
card = status.actionableStatus.card?.let(gson::toJson),
|
||||||
|
repliesCount = status.actionableStatus.repliesCount,
|
||||||
|
language = status.actionableStatus.language,
|
||||||
|
filtered = status.actionableStatus.filtered,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
primaryKeys = ["serverId", "timelineUserId"],
|
primaryKeys = ["serverId", "timelineUserId"],
|
||||||
|
@ -97,7 +143,35 @@ data class TimelineAccountEntity(
|
||||||
val avatar: String,
|
val avatar: String,
|
||||||
val emojis: String,
|
val emojis: String,
|
||||||
val bot: Boolean,
|
val bot: Boolean,
|
||||||
)
|
) {
|
||||||
|
fun toTimelineAccount(gson: Gson): TimelineAccount {
|
||||||
|
return TimelineAccount(
|
||||||
|
id = serverId,
|
||||||
|
localUsername = localUsername,
|
||||||
|
username = username,
|
||||||
|
displayName = displayName,
|
||||||
|
note = "",
|
||||||
|
url = url,
|
||||||
|
avatar = avatar,
|
||||||
|
bot = bot,
|
||||||
|
emojis = gson.fromJson(emojis, emojisListType),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(timelineAccount: TimelineAccount, accountId: Long, gson: Gson) = TimelineAccountEntity(
|
||||||
|
serverId = timelineAccount.id,
|
||||||
|
timelineUserId = accountId,
|
||||||
|
localUsername = timelineAccount.localUsername,
|
||||||
|
username = timelineAccount.username,
|
||||||
|
displayName = timelineAccount.name,
|
||||||
|
url = timelineAccount.url,
|
||||||
|
avatar = timelineAccount.avatar,
|
||||||
|
emojis = gson.toJson(timelineAccount.emojis),
|
||||||
|
bot = timelineAccount.bot,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The local view data for a status.
|
* The local view data for a status.
|
||||||
|
@ -120,6 +194,11 @@ data class StatusViewDataEntity(
|
||||||
val contentCollapsed: Boolean,
|
val contentCollapsed: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val attachmentArrayListType: Type = object : TypeToken<ArrayList<Attachment>>() {}.type
|
||||||
|
val emojisListType: Type = object : TypeToken<List<Emoji>>() {}.type
|
||||||
|
val mentionListType: Type = object : TypeToken<List<Status.Mention>>() {}.type
|
||||||
|
val tagListType: Type = object : TypeToken<List<HashTag>>() {}.type
|
||||||
|
|
||||||
data class TimelineStatusWithAccount(
|
data class TimelineStatusWithAccount(
|
||||||
@Embedded
|
@Embedded
|
||||||
val status: TimelineStatusEntity,
|
val status: TimelineStatusEntity,
|
||||||
|
@ -129,4 +208,125 @@ data class TimelineStatusWithAccount(
|
||||||
val reblogAccount: TimelineAccountEntity? = null, // null when no reblog
|
val reblogAccount: TimelineAccountEntity? = null, // null when no reblog
|
||||||
@Embedded(prefix = "svd_")
|
@Embedded(prefix = "svd_")
|
||||||
val viewData: StatusViewDataEntity? = null,
|
val viewData: StatusViewDataEntity? = null,
|
||||||
)
|
) {
|
||||||
|
fun toStatus(gson: Gson): Status {
|
||||||
|
val attachments: ArrayList<Attachment> = gson.fromJson(
|
||||||
|
status.attachments,
|
||||||
|
attachmentArrayListType,
|
||||||
|
) ?: arrayListOf()
|
||||||
|
val mentions: List<Status.Mention> = gson.fromJson(
|
||||||
|
status.mentions,
|
||||||
|
mentionListType,
|
||||||
|
) ?: emptyList()
|
||||||
|
val tags: List<HashTag>? = gson.fromJson(
|
||||||
|
status.tags,
|
||||||
|
tagListType,
|
||||||
|
)
|
||||||
|
val application = gson.fromJson(status.application, Status.Application::class.java)
|
||||||
|
val emojis: List<Emoji> = gson.fromJson(
|
||||||
|
status.emojis,
|
||||||
|
emojisListType,
|
||||||
|
) ?: emptyList()
|
||||||
|
val poll: Poll? = gson.fromJson(status.poll, Poll::class.java)
|
||||||
|
val card: Card? = gson.fromJson(status.card, Card::class.java)
|
||||||
|
|
||||||
|
val reblog = status.reblogServerId?.let { id ->
|
||||||
|
Status(
|
||||||
|
id = id,
|
||||||
|
url = status.url,
|
||||||
|
account = account.toTimelineAccount(gson),
|
||||||
|
inReplyToId = status.inReplyToId,
|
||||||
|
inReplyToAccountId = status.inReplyToAccountId,
|
||||||
|
reblog = null,
|
||||||
|
content = status.content.orEmpty(),
|
||||||
|
createdAt = Date(status.createdAt),
|
||||||
|
editedAt = status.editedAt?.let { Date(it) },
|
||||||
|
emojis = emojis,
|
||||||
|
reblogsCount = status.reblogsCount,
|
||||||
|
favouritesCount = status.favouritesCount,
|
||||||
|
reblogged = status.reblogged,
|
||||||
|
favourited = status.favourited,
|
||||||
|
bookmarked = status.bookmarked,
|
||||||
|
sensitive = status.sensitive,
|
||||||
|
spoilerText = status.spoilerText,
|
||||||
|
visibility = status.visibility,
|
||||||
|
attachments = attachments,
|
||||||
|
mentions = mentions,
|
||||||
|
tags = tags,
|
||||||
|
application = application,
|
||||||
|
pinned = false,
|
||||||
|
muted = status.muted,
|
||||||
|
poll = poll,
|
||||||
|
card = card,
|
||||||
|
repliesCount = status.repliesCount,
|
||||||
|
language = status.language,
|
||||||
|
filtered = status.filtered,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return if (reblog != null) {
|
||||||
|
Status(
|
||||||
|
id = status.serverId,
|
||||||
|
url = null, // no url for reblogs
|
||||||
|
account = reblogAccount!!.toTimelineAccount(gson),
|
||||||
|
inReplyToId = null,
|
||||||
|
inReplyToAccountId = null,
|
||||||
|
reblog = reblog,
|
||||||
|
content = "",
|
||||||
|
createdAt = Date(status.createdAt), // lie but whatever?
|
||||||
|
editedAt = null,
|
||||||
|
emojis = listOf(),
|
||||||
|
reblogsCount = 0,
|
||||||
|
favouritesCount = 0,
|
||||||
|
reblogged = false,
|
||||||
|
favourited = false,
|
||||||
|
bookmarked = false,
|
||||||
|
sensitive = false,
|
||||||
|
spoilerText = "",
|
||||||
|
visibility = status.visibility,
|
||||||
|
attachments = ArrayList(),
|
||||||
|
mentions = listOf(),
|
||||||
|
tags = listOf(),
|
||||||
|
application = null,
|
||||||
|
pinned = status.pinned,
|
||||||
|
muted = status.muted,
|
||||||
|
poll = null,
|
||||||
|
card = null,
|
||||||
|
repliesCount = status.repliesCount,
|
||||||
|
language = status.language,
|
||||||
|
filtered = status.filtered,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Status(
|
||||||
|
id = status.serverId,
|
||||||
|
url = status.url,
|
||||||
|
account = account.toTimelineAccount(gson),
|
||||||
|
inReplyToId = status.inReplyToId,
|
||||||
|
inReplyToAccountId = status.inReplyToAccountId,
|
||||||
|
reblog = null,
|
||||||
|
content = status.content.orEmpty(),
|
||||||
|
createdAt = Date(status.createdAt),
|
||||||
|
editedAt = status.editedAt?.let { Date(it) },
|
||||||
|
emojis = emojis,
|
||||||
|
reblogsCount = status.reblogsCount,
|
||||||
|
favouritesCount = status.favouritesCount,
|
||||||
|
reblogged = status.reblogged,
|
||||||
|
favourited = status.favourited,
|
||||||
|
bookmarked = status.bookmarked,
|
||||||
|
sensitive = status.sensitive,
|
||||||
|
spoilerText = status.spoilerText,
|
||||||
|
visibility = status.visibility,
|
||||||
|
attachments = attachments,
|
||||||
|
mentions = mentions,
|
||||||
|
tags = tags,
|
||||||
|
application = application,
|
||||||
|
pinned = status.pinned,
|
||||||
|
muted = status.muted,
|
||||||
|
poll = poll,
|
||||||
|
card = card,
|
||||||
|
repliesCount = status.repliesCount,
|
||||||
|
language = status.language,
|
||||||
|
filtered = status.filtered,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -261,7 +261,10 @@ class StatusViewHelper(private val itemView: View) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setupPollReadonly(poll: PollViewData?, emojis: List<Emoji>, statusDisplayOptions: StatusDisplayOptions) {
|
/**
|
||||||
|
* Configures and shows poll views based on [poll].
|
||||||
|
*/
|
||||||
|
fun setupPollReadonly(poll: PollViewData, emojis: List<Emoji>, statusDisplayOptions: StatusDisplayOptions) {
|
||||||
val pollResults = listOf<TextView>(
|
val pollResults = listOf<TextView>(
|
||||||
itemView.findViewById(R.id.status_poll_option_result_0),
|
itemView.findViewById(R.id.status_poll_option_result_0),
|
||||||
itemView.findViewById(R.id.status_poll_option_result_1),
|
itemView.findViewById(R.id.status_poll_option_result_1),
|
||||||
|
@ -271,19 +274,29 @@ class StatusViewHelper(private val itemView: View) {
|
||||||
|
|
||||||
val pollDescription = itemView.findViewById<TextView>(R.id.status_poll_description)
|
val pollDescription = itemView.findViewById<TextView>(R.id.status_poll_description)
|
||||||
|
|
||||||
if (poll == null) {
|
val timestamp = System.currentTimeMillis()
|
||||||
for (pollResult in pollResults) {
|
|
||||||
pollResult.visibility = View.GONE
|
|
||||||
}
|
|
||||||
pollDescription.visibility = View.GONE
|
|
||||||
} else {
|
|
||||||
val timestamp = System.currentTimeMillis()
|
|
||||||
|
|
||||||
setupPollResult(poll, emojis, pollResults, statusDisplayOptions.animateEmojis)
|
setupPollResult(poll, emojis, pollResults, statusDisplayOptions.animateEmojis)
|
||||||
|
|
||||||
pollDescription.visibility = View.VISIBLE
|
pollDescription.show()
|
||||||
pollDescription.text = getPollInfoText(timestamp, poll, pollDescription, statusDisplayOptions.useAbsoluteTime)
|
pollDescription.text = getPollInfoText(timestamp, poll, pollDescription, statusDisplayOptions.useAbsoluteTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides views related to polls.
|
||||||
|
*/
|
||||||
|
fun hidePoll() {
|
||||||
|
val pollResults = listOf<TextView>(
|
||||||
|
itemView.findViewById(R.id.status_poll_option_result_0),
|
||||||
|
itemView.findViewById(R.id.status_poll_option_result_1),
|
||||||
|
itemView.findViewById(R.id.status_poll_option_result_2),
|
||||||
|
itemView.findViewById(R.id.status_poll_option_result_3),
|
||||||
|
)
|
||||||
|
|
||||||
|
for (pollResult in pollResults) {
|
||||||
|
pollResult.hide()
|
||||||
}
|
}
|
||||||
|
itemView.findViewById<TextView>(R.id.status_poll_description).hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPollInfoText(timestamp: Long, poll: PollViewData, pollDescription: TextView, useAbsoluteTime: Boolean): CharSequence {
|
private fun getPollInfoText(timestamp: Long, poll: PollViewData, pollDescription: TextView, useAbsoluteTime: Boolean): CharSequence {
|
||||||
|
|
|
@ -1,97 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2023 Tusky Contributors
|
|
||||||
*
|
|
||||||
* This file is a part of Pachli.
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*
|
|
||||||
* Pachli 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 Tusky; if not,
|
|
||||||
* see <http://www.gnu.org/licenses>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@file:JvmName("ViewDataUtils")
|
|
||||||
|
|
||||||
/* Copyright 2017 Andrew Dawson
|
|
||||||
*
|
|
||||||
* This file is a part of Pachli.
|
|
||||||
*
|
|
||||||
* 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.
|
|
||||||
*
|
|
||||||
* Pachli 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 Tusky; if not,
|
|
||||||
* see <http://www.gnu.org/licenses>. */
|
|
||||||
package app.pachli.util
|
|
||||||
|
|
||||||
import app.pachli.entity.Filter
|
|
||||||
import app.pachli.entity.Notification
|
|
||||||
import app.pachli.entity.Status
|
|
||||||
import app.pachli.entity.TrendingTag
|
|
||||||
import app.pachli.viewdata.NotificationViewData
|
|
||||||
import app.pachli.viewdata.StatusViewData
|
|
||||||
import app.pachli.viewdata.TrendingViewData
|
|
||||||
|
|
||||||
fun Status.toViewData(
|
|
||||||
isShowingContent: Boolean,
|
|
||||||
isExpanded: Boolean,
|
|
||||||
isCollapsed: Boolean,
|
|
||||||
isDetailed: Boolean = false,
|
|
||||||
filterAction: Filter.Action = app.pachli.entity.Filter.Action.NONE,
|
|
||||||
): StatusViewData {
|
|
||||||
return StatusViewData(
|
|
||||||
status = this,
|
|
||||||
isShowingContent = isShowingContent,
|
|
||||||
isCollapsed = isCollapsed,
|
|
||||||
isExpanded = isExpanded,
|
|
||||||
isDetailed = isDetailed,
|
|
||||||
filterAction = filterAction,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Notification.toViewData(
|
|
||||||
isShowingContent: Boolean,
|
|
||||||
isExpanded: Boolean,
|
|
||||||
isCollapsed: Boolean,
|
|
||||||
filterAction: Filter.Action,
|
|
||||||
): NotificationViewData {
|
|
||||||
return NotificationViewData(
|
|
||||||
this.type,
|
|
||||||
this.id,
|
|
||||||
this.account,
|
|
||||||
this.status?.toViewData(
|
|
||||||
isShowingContent,
|
|
||||||
isExpanded,
|
|
||||||
isCollapsed,
|
|
||||||
filterAction = filterAction,
|
|
||||||
),
|
|
||||||
this.report,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun List<TrendingTag>.toViewData(): List<TrendingViewData.Tag> {
|
|
||||||
val maxTrendingValue = flatMap { tag -> tag.history }
|
|
||||||
.mapNotNull { it.uses.toLongOrNull() }
|
|
||||||
.maxOrNull() ?: 1
|
|
||||||
|
|
||||||
return map { tag ->
|
|
||||||
|
|
||||||
val reversedHistory = tag.history.asReversed()
|
|
||||||
|
|
||||||
TrendingViewData.Tag(
|
|
||||||
name = tag.name,
|
|
||||||
usage = reversedHistory.mapNotNull { it.uses.toLongOrNull() },
|
|
||||||
accounts = reversedHistory.mapNotNull { it.accounts.toLongOrNull() },
|
|
||||||
maxTrendingValue = maxTrendingValue,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package app.pachli.viewdata
|
package app.pachli.viewdata
|
||||||
|
|
||||||
|
import app.pachli.entity.Filter
|
||||||
import app.pachli.entity.Notification
|
import app.pachli.entity.Notification
|
||||||
import app.pachli.entity.Report
|
import app.pachli.entity.Report
|
||||||
import app.pachli.entity.TimelineAccount
|
import app.pachli.entity.TimelineAccount
|
||||||
|
@ -27,4 +28,28 @@ data class NotificationViewData(
|
||||||
val account: TimelineAccount,
|
val account: TimelineAccount,
|
||||||
var statusViewData: StatusViewData?,
|
var statusViewData: StatusViewData?,
|
||||||
val report: Report?,
|
val report: Report?,
|
||||||
)
|
) {
|
||||||
|
companion object {
|
||||||
|
fun from(
|
||||||
|
notification: Notification,
|
||||||
|
isShowingContent: Boolean,
|
||||||
|
isExpanded: Boolean,
|
||||||
|
isCollapsed: Boolean,
|
||||||
|
filterAction: Filter.Action,
|
||||||
|
) = NotificationViewData(
|
||||||
|
notification.type,
|
||||||
|
notification.id,
|
||||||
|
notification.account,
|
||||||
|
notification.status?.let { status ->
|
||||||
|
StatusViewData.from(
|
||||||
|
status,
|
||||||
|
isShowingContent,
|
||||||
|
isExpanded,
|
||||||
|
isCollapsed,
|
||||||
|
filterAction = filterAction,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
notification.report,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -34,14 +34,36 @@ data class PollViewData(
|
||||||
val votersCount: Int?,
|
val votersCount: Int?,
|
||||||
val options: List<PollOptionViewData>,
|
val options: List<PollOptionViewData>,
|
||||||
var voted: Boolean,
|
var voted: Boolean,
|
||||||
)
|
) {
|
||||||
|
companion object {
|
||||||
|
fun from(poll: Poll) = PollViewData(
|
||||||
|
id = poll.id,
|
||||||
|
expiresAt = poll.expiresAt,
|
||||||
|
expired = poll.expired,
|
||||||
|
multiple = poll.multiple,
|
||||||
|
votesCount = poll.votesCount,
|
||||||
|
votersCount = poll.votersCount,
|
||||||
|
options = poll.options.mapIndexed { index, option -> PollOptionViewData.from(option, poll.ownVotes?.contains(index) == true) },
|
||||||
|
voted = poll.voted,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class PollOptionViewData(
|
data class PollOptionViewData(
|
||||||
val title: String,
|
val title: String,
|
||||||
var votesCount: Int,
|
var votesCount: Int,
|
||||||
var selected: Boolean,
|
var selected: Boolean,
|
||||||
var voted: Boolean,
|
var voted: Boolean,
|
||||||
)
|
) {
|
||||||
|
companion object {
|
||||||
|
fun from(pollOption: PollOption, voted: Boolean) = PollOptionViewData(
|
||||||
|
title = pollOption.title,
|
||||||
|
votesCount = pollOption.votesCount,
|
||||||
|
selected = false,
|
||||||
|
voted = voted,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun calculatePercent(fraction: Int, totalVoters: Int?, totalVotes: Int): Int {
|
fun calculatePercent(fraction: Int, totalVoters: Int?, totalVotes: Int): Int {
|
||||||
return if (fraction == 0) {
|
return if (fraction == 0) {
|
||||||
|
@ -61,26 +83,3 @@ fun buildDescription(title: String, percent: Int, voted: Boolean, context: Conte
|
||||||
}
|
}
|
||||||
return builder.append(title)
|
return builder.append(title)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Poll?.toViewData(): PollViewData? {
|
|
||||||
if (this == null) return null
|
|
||||||
return PollViewData(
|
|
||||||
id = id,
|
|
||||||
expiresAt = expiresAt,
|
|
||||||
expired = expired,
|
|
||||||
multiple = multiple,
|
|
||||||
votesCount = votesCount,
|
|
||||||
votersCount = votersCount,
|
|
||||||
options = options.mapIndexed { index, option -> option.toViewData(ownVotes?.contains(index) == true) },
|
|
||||||
voted = voted,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun PollOption.toViewData(voted: Boolean): PollOptionViewData {
|
|
||||||
return PollOptionViewData(
|
|
||||||
title = title,
|
|
||||||
votesCount = votesCount,
|
|
||||||
selected = false,
|
|
||||||
voted = voted,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
@ -16,11 +16,16 @@ package app.pachli.viewdata
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
|
import app.pachli.components.conversation.ConversationAccountEntity
|
||||||
|
import app.pachli.components.conversation.ConversationStatusEntity
|
||||||
|
import app.pachli.db.TimelineStatusWithAccount
|
||||||
import app.pachli.entity.Filter
|
import app.pachli.entity.Filter
|
||||||
|
import app.pachli.entity.Poll
|
||||||
import app.pachli.entity.Status
|
import app.pachli.entity.Status
|
||||||
import app.pachli.util.parseAsMastodonHtml
|
import app.pachli.util.parseAsMastodonHtml
|
||||||
import app.pachli.util.replaceCrashingCharacters
|
import app.pachli.util.replaceCrashingCharacters
|
||||||
import app.pachli.util.shouldTrimStatus
|
import app.pachli.util.shouldTrimStatus
|
||||||
|
import com.google.gson.Gson
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data required to display a status.
|
* Data required to display a status.
|
||||||
|
@ -113,4 +118,111 @@ data class StatusViewData(
|
||||||
|
|
||||||
/** Helper for Java */
|
/** Helper for Java */
|
||||||
fun copyWithCollapsed(isCollapsed: Boolean) = copy(isCollapsed = isCollapsed)
|
fun copyWithCollapsed(isCollapsed: Boolean) = copy(isCollapsed = isCollapsed)
|
||||||
|
|
||||||
|
fun toConversationStatusEntity(
|
||||||
|
favourited: Boolean = status.favourited,
|
||||||
|
bookmarked: Boolean = status.bookmarked,
|
||||||
|
muted: Boolean = status.muted ?: false,
|
||||||
|
poll: Poll? = status.poll,
|
||||||
|
expanded: Boolean = isExpanded,
|
||||||
|
collapsed: Boolean = isCollapsed,
|
||||||
|
showingHiddenContent: Boolean = isShowingContent,
|
||||||
|
) = ConversationStatusEntity(
|
||||||
|
id = id,
|
||||||
|
url = status.url,
|
||||||
|
inReplyToId = status.inReplyToId,
|
||||||
|
inReplyToAccountId = status.inReplyToAccountId,
|
||||||
|
account = ConversationAccountEntity.from(status.account),
|
||||||
|
content = status.content,
|
||||||
|
createdAt = status.createdAt,
|
||||||
|
editedAt = status.editedAt,
|
||||||
|
emojis = status.emojis,
|
||||||
|
favouritesCount = status.favouritesCount,
|
||||||
|
repliesCount = status.repliesCount,
|
||||||
|
favourited = favourited,
|
||||||
|
bookmarked = bookmarked,
|
||||||
|
sensitive = status.sensitive,
|
||||||
|
spoilerText = status.spoilerText,
|
||||||
|
attachments = status.attachments,
|
||||||
|
mentions = status.mentions,
|
||||||
|
tags = status.tags,
|
||||||
|
showingHiddenContent = showingHiddenContent,
|
||||||
|
expanded = expanded,
|
||||||
|
collapsed = collapsed,
|
||||||
|
muted = muted,
|
||||||
|
poll = poll,
|
||||||
|
language = status.language,
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(
|
||||||
|
status: Status,
|
||||||
|
isShowingContent: Boolean,
|
||||||
|
isExpanded: Boolean,
|
||||||
|
isCollapsed: Boolean,
|
||||||
|
isDetailed: Boolean = false,
|
||||||
|
filterAction: Filter.Action = Filter.Action.NONE,
|
||||||
|
) = StatusViewData(
|
||||||
|
status = status,
|
||||||
|
isShowingContent = isShowingContent,
|
||||||
|
isCollapsed = isCollapsed,
|
||||||
|
isExpanded = isExpanded,
|
||||||
|
isDetailed = isDetailed,
|
||||||
|
filterAction = filterAction,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun from(conversationStatusEntity: ConversationStatusEntity) = StatusViewData(
|
||||||
|
status = Status(
|
||||||
|
id = conversationStatusEntity.id,
|
||||||
|
url = conversationStatusEntity.url,
|
||||||
|
account = conversationStatusEntity.account.toAccount(),
|
||||||
|
inReplyToId = conversationStatusEntity.inReplyToId,
|
||||||
|
inReplyToAccountId = conversationStatusEntity.inReplyToAccountId,
|
||||||
|
content = conversationStatusEntity.content,
|
||||||
|
reblog = null,
|
||||||
|
createdAt = conversationStatusEntity.createdAt,
|
||||||
|
editedAt = conversationStatusEntity.editedAt,
|
||||||
|
emojis = conversationStatusEntity.emojis,
|
||||||
|
reblogsCount = 0,
|
||||||
|
favouritesCount = conversationStatusEntity.favouritesCount,
|
||||||
|
repliesCount = conversationStatusEntity.repliesCount,
|
||||||
|
reblogged = false,
|
||||||
|
favourited = conversationStatusEntity.favourited,
|
||||||
|
bookmarked = conversationStatusEntity.bookmarked,
|
||||||
|
sensitive = conversationStatusEntity.sensitive,
|
||||||
|
spoilerText = conversationStatusEntity.spoilerText,
|
||||||
|
visibility = Status.Visibility.DIRECT,
|
||||||
|
attachments = conversationStatusEntity.attachments,
|
||||||
|
mentions = conversationStatusEntity.mentions,
|
||||||
|
tags = conversationStatusEntity.tags,
|
||||||
|
application = null,
|
||||||
|
pinned = false,
|
||||||
|
muted = conversationStatusEntity.muted,
|
||||||
|
poll = conversationStatusEntity.poll,
|
||||||
|
card = null,
|
||||||
|
language = conversationStatusEntity.language,
|
||||||
|
filtered = null,
|
||||||
|
),
|
||||||
|
isExpanded = conversationStatusEntity.expanded,
|
||||||
|
isShowingContent = conversationStatusEntity.showingHiddenContent,
|
||||||
|
isCollapsed = conversationStatusEntity.collapsed,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun from(
|
||||||
|
timelineStatusWithAccount: TimelineStatusWithAccount,
|
||||||
|
gson: Gson,
|
||||||
|
isExpanded: Boolean,
|
||||||
|
isShowingContent: Boolean,
|
||||||
|
isDetailed: Boolean = false,
|
||||||
|
): StatusViewData {
|
||||||
|
val status = timelineStatusWithAccount.toStatus(gson)
|
||||||
|
return StatusViewData(
|
||||||
|
status = status,
|
||||||
|
isExpanded = timelineStatusWithAccount.viewData?.expanded ?: isExpanded,
|
||||||
|
isShowingContent = timelineStatusWithAccount.viewData?.contentShowing ?: (isShowingContent || !status.actionableStatus.sensitive),
|
||||||
|
isCollapsed = timelineStatusWithAccount.viewData?.contentCollapsed ?: true,
|
||||||
|
isDetailed = isDetailed,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
package app.pachli.viewdata
|
package app.pachli.viewdata
|
||||||
|
|
||||||
|
import app.pachli.entity.TrendingTag
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
sealed class TrendingViewData {
|
sealed class TrendingViewData {
|
||||||
|
@ -36,5 +37,19 @@ sealed class TrendingViewData {
|
||||||
) : TrendingViewData() {
|
) : TrendingViewData() {
|
||||||
override val id: String
|
override val id: String
|
||||||
get() = name
|
get() = name
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(trendingTag: TrendingTag, maxTrendingValue: Long): Tag {
|
||||||
|
// Reverse the list to put oldest items first
|
||||||
|
val reversedHistory = trendingTag.history.asReversed()
|
||||||
|
|
||||||
|
return Tag(
|
||||||
|
name = trendingTag.name,
|
||||||
|
usage = reversedHistory.map { it.uses.toLongOrNull() ?: 0 },
|
||||||
|
accounts = reversedHistory.map { it.accounts.toLongOrNull() ?: 0 },
|
||||||
|
maxTrendingValue = maxTrendingValue,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package app.pachli.components.timeline
|
package app.pachli.components.timeline
|
||||||
|
|
||||||
import app.pachli.db.StatusViewDataEntity
|
import app.pachli.db.StatusViewDataEntity
|
||||||
|
import app.pachli.db.TimelineAccountEntity
|
||||||
|
import app.pachli.db.TimelineStatusEntity
|
||||||
import app.pachli.db.TimelineStatusWithAccount
|
import app.pachli.db.TimelineStatusWithAccount
|
||||||
import app.pachli.entity.Status
|
import app.pachli.entity.Status
|
||||||
import app.pachli.entity.TimelineAccount
|
import app.pachli.entity.TimelineAccount
|
||||||
|
@ -95,11 +97,13 @@ fun mockStatusEntityWithAccount(
|
||||||
val gson = Gson()
|
val gson = Gson()
|
||||||
|
|
||||||
return TimelineStatusWithAccount(
|
return TimelineStatusWithAccount(
|
||||||
status = mockedStatus.toEntity(
|
status = TimelineStatusEntity.from(
|
||||||
|
mockedStatus,
|
||||||
timelineUserId = userId,
|
timelineUserId = userId,
|
||||||
gson = gson,
|
gson = gson,
|
||||||
),
|
),
|
||||||
account = mockedStatus.account.toEntity(
|
account = TimelineAccountEntity.from(
|
||||||
|
mockedStatus.account,
|
||||||
accountId = userId,
|
accountId = userId,
|
||||||
gson = gson,
|
gson = gson,
|
||||||
),
|
),
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
# Code style guide
|
||||||
|
|
||||||
|
## Synopsis
|
||||||
|
|
||||||
|
This document describes aspects of code style that are not enforced with linters or formatting tools but the project still tries to adhere to. Some of these are things that developers might reasonably disagree on, but the project has a specific stance.
|
||||||
|
|
||||||
|
## Topics
|
||||||
|
|
||||||
|
### On converting between types
|
||||||
|
|
||||||
|
Roughly speaking the code that handles data from the user's server deals with three variations of that data.
|
||||||
|
|
||||||
|
1. Data from the network
|
||||||
|
2. Data cached locally (either to disk or memory)
|
||||||
|
3. Data displayed to the user
|
||||||
|
|
||||||
|
There must be code to convert between those representations, and it's important to make sure there isn't a loop in the dependency graph between the types.
|
||||||
|
|
||||||
|
Consider two types, `N`, representing data received from the network, and `C`, representing data that will be cached.
|
||||||
|
|
||||||
|
The wrong way to do it is code like:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
/** In N.kt (the network data type) */
|
||||||
|
import C
|
||||||
|
|
||||||
|
data class N() {
|
||||||
|
fun toC(): C { /* return a C created from a N */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---
|
||||||
|
|
||||||
|
/** in C.kt (the cache data type) */
|
||||||
|
import N
|
||||||
|
|
||||||
|
data class C() {
|
||||||
|
fun toN(): N { /* return a N created from a C */ }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates a loop in their dependency graph as they import each other's types.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
direction RL
|
||||||
|
class N{
|
||||||
|
toC() C
|
||||||
|
}
|
||||||
|
|
||||||
|
class C{
|
||||||
|
toN() N
|
||||||
|
}
|
||||||
|
|
||||||
|
C ..> N: Imports
|
||||||
|
N ..> C: Imports
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a problem because:
|
||||||
|
|
||||||
|
- They can't be placed in separate modules
|
||||||
|
- Modifying code in `N` can cause `C` to be recompiled, and vice-versa
|
||||||
|
|
||||||
|
To fix this:
|
||||||
|
|
||||||
|
1. Pick one type as being "higher" in the dependency tree than the other
|
||||||
|
2. Remove the `to...()` method from the lower type, and implement it as a companion `from()` method on the higher type
|
||||||
|
|
||||||
|
In Pachli the dependency hierarchy is (higher types on the left):
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
ViewData --> ViewModel --> Cache --> Network --> Core
|
||||||
|
```
|
||||||
|
|
||||||
|
so the previous example involving a network type and a cache type would instead be written as:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
/** In N.kt (the network data type) */
|
||||||
|
data class N() {
|
||||||
|
// No import, no toC() method
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---
|
||||||
|
|
||||||
|
/** in C.kt (the cache data type) */
|
||||||
|
import N
|
||||||
|
|
||||||
|
data class C() {
|
||||||
|
fun toN(): N { /* return a N created from a C */ }
|
||||||
|
companion object {
|
||||||
|
fun from(n: N): C {
|
||||||
|
// code from the N.toC() in the previous example
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now the dependency between the two types is:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
direction RL
|
||||||
|
class N{
|
||||||
|
}
|
||||||
|
|
||||||
|
class C{
|
||||||
|
toN() N
|
||||||
|
from(n: N)$ C
|
||||||
|
}
|
||||||
|
|
||||||
|
C <.. N: Imports
|
||||||
|
```
|
||||||
|
|
||||||
|
The circular dependency is gone, the `N` type can easily be placed in a separate module to the `C` type, and changes to the `C` type will not require the `N` type to be recompiled.
|
||||||
|
|
||||||
|
In these examples the `from` method could also have been written as a secondary constructor instead of a static factory method in the companion object. We prefer static factory methods over secondary constructors because:
|
||||||
|
|
||||||
|
1. Functions have names that can more clearly indicate their intent
|
||||||
|
2. Functions can return objects of any subtype. If the example class `C` had multiple subtypes the correct subtype could be returned based on properties of `N`
|
||||||
|
3. Functions can return null or other values to signify an error. Perhaps the network type is expected to contain a particular property, but the server has a bug and returned data without that property.
|
||||||
|
4. Functions can have more specific visibility modifiers
|
||||||
|
5. Functions can be marked `inline`
|
|
@ -93,7 +93,7 @@ Pull requests (PRs) are the primary unit of collaboration for code.
|
||||||
|
|
||||||
### Work on branches in your own fork of the repository
|
### Work on branches in your own fork of the repository
|
||||||
|
|
||||||
Do not clone the `pachli-android` repository. Instead, create a fork, create a branch in your fork from the `main` branch, and commit your changes to that.
|
Do not clone the `pachli-android` repository. Instead, create a fork, create a branch in your fork from the `main` branch, and commit your changes to that branch.
|
||||||
|
|
||||||
See the GitHub [Collaborating with pull requests](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/getting-started/about-collaborative-development-models) documentation.
|
See the GitHub [Collaborating with pull requests](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/getting-started/about-collaborative-development-models) documentation.
|
||||||
|
|
||||||
|
@ -114,6 +114,32 @@ Typically you would configure the build variant in Android Studio with Build > S
|
||||||
|
|
||||||
This is not mandatory, but may make developing easier for you.
|
This is not mandatory, but may make developing easier for you.
|
||||||
|
|
||||||
|
### Code style
|
||||||
|
|
||||||
|
#### `ktlintCheck` and `ktlintFormat`
|
||||||
|
|
||||||
|
The project uses [ktlint](https://pinterest.github.io/ktlint/) to enforce common code and formatting standards.
|
||||||
|
|
||||||
|
You can check your code before creating the PR with the `ktlintCheck` task.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
./gradlew ktlintCheck
|
||||||
|
```
|
||||||
|
|
||||||
|
Most code formatting issues can be automatically resolved with the `ktlintFormat` task.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
./gradlew ktlintFormat
|
||||||
|
```
|
||||||
|
|
||||||
|
The code in your PR will be checked for this every time it changes. If it is not lint-clean and automated fixes are possible they will be added as comments to the PR.
|
||||||
|
|
||||||
|
#### Questions of taste
|
||||||
|
|
||||||
|
Some code style issues are questions of taste, where developers might reasonably differ but the project has a specific stance.
|
||||||
|
|
||||||
|
Please read the [Code style guide](/docs/code-style.md).
|
||||||
|
|
||||||
### Individual commits
|
### Individual commits
|
||||||
|
|
||||||
A PR is typically made up multiple commits.
|
A PR is typically made up multiple commits.
|
||||||
|
@ -148,7 +174,7 @@ This makes things needlessly difficult for your reviewers.
|
||||||
The project uses the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) standard for commit messages. If you are not familiar with them [Conventional Commits: A better way](https://medium.com/neudesic-innovation/conventional-commits-a-better-way-78d6785c2e08) is also a good introduction.
|
The project uses the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) standard for commit messages. If you are not familiar with them [Conventional Commits: A better way](https://medium.com/neudesic-innovation/conventional-commits-a-better-way-78d6785c2e08) is also a good introduction.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> See [docs/decisions/0001-use-conventional-commits.md](https://github.com/pachli/pachli-android/docs/decisions/0001-use-conventional-commits.md)
|
> See [docs/decisions/0001-use-conventional-commits.md](/docs/decisions/0001-use-conventional-commits.md)
|
||||||
|
|
||||||
The PR's title and description will become the first line and remaining body of the commit message when the PR is merged, so your PR title and description should also follow the conventional commits approach.
|
The PR's title and description will become the first line and remaining body of the commit message when the PR is merged, so your PR title and description should also follow the conventional commits approach.
|
||||||
|
|
||||||
|
@ -175,7 +201,7 @@ The types are:
|
||||||
- `test`, modify the test suite
|
- `test`, modify the test suite
|
||||||
- `wip`, work-in-progress
|
- `wip`, work-in-progress
|
||||||
|
|
||||||
More details on each is in [docs/decisions/conventional-commits.md](https://github.com/pachli/pachli-android/docs/decisions/conventional-commits.md).
|
More details on each is in [docs/decisions/0001-use-conventional-commits.md](/docs/decisions/0001-use-conventional-commits.md).
|
||||||
|
|
||||||
`feat` for new features and `fix` for bug fixes are the most common.
|
`feat` for new features and `fix` for bug fixes are the most common.
|
||||||
|
|
||||||
|
@ -250,24 +276,6 @@ You should periodically merge changes from the `main` branch in to your PR branc
|
||||||
|
|
||||||
If your PR can not be cleanly merged in to `main` it is difficult to review effectively, because merging the changes from `main` in to your PR will invalidate the review. You've changed the code, so the reviewer needs to look at it again.
|
If your PR can not be cleanly merged in to `main` it is difficult to review effectively, because merging the changes from `main` in to your PR will invalidate the review. You've changed the code, so the reviewer needs to look at it again.
|
||||||
|
|
||||||
#### `ktlintCheck` and `ktlintFormat`
|
|
||||||
|
|
||||||
The project uses [ktlint](https://pinterest.github.io/ktlint/) to enforce common code and formatting standards.
|
|
||||||
|
|
||||||
You can check your code before creating the PR with the `ktlintCheck` task.
|
|
||||||
|
|
||||||
```shell
|
|
||||||
./gradlew ktlintCheck
|
|
||||||
```
|
|
||||||
|
|
||||||
Most code formatting issues can be automatically resolved with the `ktlintFormat` task.
|
|
||||||
|
|
||||||
```shell
|
|
||||||
./gradlew ktlintFormat
|
|
||||||
```
|
|
||||||
|
|
||||||
The code in your PR will be checked for this every time it changes. If it is not lint-clean and automated fixes are possible they will be added as comments to the PR.
|
|
||||||
|
|
||||||
#### Tests
|
#### Tests
|
||||||
|
|
||||||
The project has a number of automated tests, they will automatically be run on your PR when it is submitted.
|
The project has a number of automated tests, they will automatically be run on your PR when it is submitted.
|
||||||
|
|
Loading…
Reference in New Issue