Enhance the context menu for private messages (#1053)

* Enhance the message context menu

A direct message can now directly be bookmarked, the author can be publicly
mentioned and reported.

Signed-off-by: Paul Schuetz <pa.schuetz@web.de>

* Add options to the conversation list context menu

Since the latest message is shown in the conversation list, the user can now
interact with this message via the context menu similar to the messages in the
conversation history.
The "conversation" class had to be modified since
bookmarking and liking a message would have led to a race condition (depending
on the server) when fetching the conversations afterwards, so the only affected
the message is now immediately updated.

Signed-off-by: Paul Schuetz <pa.schuetz@web.de>

* Remove child view models

The child views models are removed, and the list row now only uses the conversation
object managed by the list view model.

Signed-off-by: Paul Schuetz <pa.schuetz@web.de>

* Make unmodified var let

The last state-var of a conversation isn't modified, instead, a new conversation
is created. Therefore, the var is now a let.

Signed-off-by: Paul Schuetz <pa.schuetz@web.de>

---------

Signed-off-by: Paul Schuetz <pa.schuetz@web.de>
This commit is contained in:
Paul Schuetz 2023-02-26 06:45:31 +01:00 committed by GitHub
parent 2ba2675ae4
commit 06629cc397
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 171 additions and 26 deletions

View file

@ -296,6 +296,7 @@
"conversations.error.title" = "Памылка падчас загрузкі вашых паведамленняў";
"conversations.navigation-title" = "Непасрэдныя паведамленні";
"conversations.new.message.placeholder" = "Новае паведамленне";
"conversations.latest.message" = "Latest Message";
// MARK: Package: DesignSystem
"design.tag.n-posts-from-n-participants %lld %lld" = "%lld допісы ад %lld удзельнікаў";

View file

@ -290,6 +290,7 @@
"conversations.error.title" = "S'ha produït un error";
"conversations.navigation-title" = "Missatges directes";
"conversations.new.message.placeholder" = "Missatge nou";
"conversations.latest.message" = "Latest Message";
// MARK: Package: DesignSystem
"design.tag.n-posts-from-n-participants %lld %lld" = "%lld publicacions de %lld participants";

View file

@ -292,6 +292,7 @@
"conversations.error.title" = "Ein Fehler ist aufgetreten";
"conversations.navigation-title" = "Direkte Nachrichten";
"conversations.new.message.placeholder" = "Neue Nachricht";
"conversations.latest.message" = "Letzte Nachricht";
// MARK: Package: DesignSystem
"design.tag.n-posts-from-n-participants %lld %lld" = "%lld Beiträge von %lld Teilnehmenden";

View file

@ -293,6 +293,7 @@
"conversations.error.title" = "An error occurred";
"conversations.navigation-title" = "Direct Messages";
"conversations.new.message.placeholder" = "New Message";
"conversations.latest.message" = "Latest Message";
// MARK: Package: DesignSystem
"design.tag.n-posts-from-n-participants %lld %lld" = "%lld posts from %lld participants";

View file

@ -292,6 +292,7 @@
"conversations.error.title" = "An error occurred";
"conversations.navigation-title" = "Direct Messages";
"conversations.new.message.placeholder" = "New Message";
"conversations.latest.message" = "Latest Message";
// MARK: Package: DesignSystem
"design.tag.n-posts-from-n-participants %lld %lld" = "%lld posts from %lld participants";

View file

@ -292,6 +292,7 @@
"conversations.error.title" = "Ha ocurrido un error";
"conversations.navigation-title" = "Mensajes directos";
"conversations.new.message.placeholder" = "Mensajes nuevos";
"conversations.latest.message" = "Latest Message";
// MARK: Package: DesignSystem
"design.tag.n-posts-from-n-participants %lld %lld" = "%lld publicaciones de %lld participantes";

View file

@ -292,6 +292,7 @@
"conversations.error.title" = "Errorea gertatu da";
"conversations.navigation-title" = "Mezu zuzenak";
"conversations.new.message.placeholder" = "Mezu berria";
"conversations.latest.message" = "Latest Message";
// MARK: Package: DesignSystem
"design.theme.navigation-title" = "Gai hautatzailea";

View file

@ -291,6 +291,7 @@
"conversations.error.title" = "Une erreur est survenue";
"conversations.navigation-title" = "Messages directs";
"conversations.new.message.placeholder" = "Nouveau message";
"conversations.latest.message" = "Latest Message";
// MARK: Package: DesignSystem
"design.tag.n-posts-from-n-participants %lld %lld" = "%lld publications de %lld participants";

View file

@ -292,6 +292,7 @@
"conversations.error.title" = "Si è verificato in errore";
"conversations.navigation-title" = "Messaggi diretti";
"conversations.new.message.placeholder" = "Nuovo messaggio";
"conversations.latest.message" = "Latest Message";
// MARK: Package: DesignSystem
"design.tag.n-posts-from-n-participants %lld %lld" = "%lld post da %lld partecipanti";

View file

@ -291,6 +291,7 @@
"conversations.error.title" = "エラーが発生しました";
"conversations.navigation-title" = "ダイレクトメッセージ";
"conversations.new.message.placeholder" = "新しいメッセージ";
"conversations.latest.message" = "Latest Message";
// MARK: Package: DesignSystem
"design.tag.n-posts-from-n-participants %lld %lld" = "%lld トゥートの投稿 %lld 人が投稿している";

View file

@ -292,6 +292,7 @@
"conversations.error.title" = "오류";
"conversations.navigation-title" = "다이렉트 메시지";
"conversations.new.message.placeholder" = "새 메시지";
"conversations.latest.message" = "Latest Message";
// MARK: Package: DesignSystem
"design.tag.n-posts-from-n-participants %lld %lld" = "%lld개 글 (%lld명이 이야기 중)";

View file

@ -291,6 +291,7 @@
"conversations.error.title" = "En feil oppstod";
"conversations.navigation-title" = "Direktemeldinger";
"conversations.new.message.placeholder" = "Ny melding";
"conversations.latest.message" = "Latest Message";
// MARK: Package: DesignSystem
"design.tag.n-posts-from-n-participants %lld %lld" = "%lld innlegg fra %lld deltakere";

View file

@ -289,6 +289,7 @@
"conversations.error.title" = "Er heeft zich een fout voorgedaan";
"conversations.navigation-title" = "Directe berichten";
"conversations.new.message.placeholder" = "Nieuw bericht";
"conversations.latest.message" = "Latest Message";
// MARK: Package: DesignSystem
"design.tag.n-posts-from-n-participants %lld %lld" = "%lld posts van %lld deelnemers";

View file

@ -289,6 +289,7 @@
"conversations.error.title" = "Wystąpił błąd";
"conversations.navigation-title" = "Wiadomości bezpośrednie";
"conversations.new.message.placeholder" = "Nowa wiadomość";
"conversations.latest.message" = "Latest Message";
// MARK: Package: DesignSystem
"design.theme.navigation-title" = "Wybór motywu";

View file

@ -291,6 +291,7 @@
"conversations.error.title" = "Ocorreu um erro";
"conversations.navigation-title" = "Mensagens diretas";
"conversations.new.message.placeholder" = "Nova mensagem";
"conversations.latest.message" = "Latest Message";
// MARK: Package: DesignSystem
"design.tag.n-posts-from-n-participants %lld %lld" = "%lld postagens de %lld participantes";

View file

@ -287,6 +287,7 @@
"conversations.error.title" = "Bir hata oluştu";
"conversations.navigation-title" = "Direkt Mesajlar";
"conversations.new.message.placeholder" = "Yeni Mesaj";
"conversations.latest.message" = "Latest Message";
// MARK: Package: DesignSystem
"design.tag.n-posts-from-n-participants %lld %lld" = "%lld katılımcılar tarafından %lld gönderi";

View file

@ -292,6 +292,7 @@
"conversations.error.title" = "Виникла халепа";
"conversations.navigation-title" = "Особисті повідомлення";
"conversations.new.message.placeholder" = "Нове повідомлення";
"conversations.latest.message" = "Latest Message";
// MARK: Package: DesignSystem
"design.tag.n-posts-from-n-participants %lld %lld" = "%lld дописів від %lld учасників";

View file

@ -290,6 +290,7 @@
"conversations.error.title" = "出错啦";
"conversations.navigation-title" = "私信";
"conversations.new.message.placeholder" = "新消息";
"conversations.latest.message" = "Latest Message";
// MARK: Package: DesignSystem
"design.tag.n-posts-from-n-participants %lld %lld" = "%lld 条嘟文来自 %lld 个参与者";

View file

@ -16,6 +16,7 @@ struct ConversationMessageView: View {
let conversation: Conversation
@State private var isLiked: Bool = false
@State private var isBookmarked: Bool = false
var body: some View {
let isOwnMessage = message.account.id == currentAccount.account?.id
@ -82,6 +83,7 @@ struct ConversationMessageView: View {
}
.onAppear {
isLiked = message.favourited == true
isBookmarked = message.bookmarked == true
}
}
@ -115,6 +117,22 @@ struct ConversationMessageView: View {
Label(isLiked ? "status.action.unfavorite" : "status.action.favorite",
systemImage: isLiked ? "star.fill" : "star")
}
Button { Task {
do {
let status: Status
if isBookmarked {
status = try await client.post(endpoint: Statuses.unbookmark(id: message.id))
} else {
status = try await client.post(endpoint: Statuses.bookmark(id: message.id))
}
withAnimation {
isBookmarked = status.bookmarked == true
}
} catch {}
} } label: {
Label(isBookmarked ? "status.action.unbookmark" : "status.action.bookmark",
systemImage: isBookmarked ? "bookmark.fill" : "bookmark")
}
Divider()
if message.account.id == currentAccount.account?.id {
Button("status.action.delete", role: .destructive) {
@ -122,6 +140,21 @@ struct ConversationMessageView: View {
_ = try await client.delete(endpoint: Statuses.status(id: message.id))
}
}
} else {
Section(message.reblog?.account.acct ?? message.account.acct) {
Button {
routerPath.presentedSheet = .mentionStatusEditor(account: message.reblog?.account ?? message.account, visibility: .pub)
} label: {
Label("status.action.mention", systemImage: "at")
}
}
Section {
Button(role: .destructive) {
routerPath.presentedSheet = .report(status: message.reblogAsAsStatus ?? message)
} label: {
Label("status.action.report", systemImage: "exclamationmark.bubble")
}
}
}
}

View file

@ -9,8 +9,9 @@ struct ConversationsListRow: View {
@EnvironmentObject private var client: Client
@EnvironmentObject private var routerPath: RouterPath
@EnvironmentObject private var theme: Theme
let conversation: Conversation
@EnvironmentObject private var currentAccount: CurrentAccount
@Binding var conversation: Conversation
@ObservedObject var viewModel: ConversationsListViewModel
var body: some View {
@ -32,8 +33,8 @@ struct ConversationsListRow: View {
.foregroundColor(theme.tintColor)
.frame(width: 10, height: 10)
}
if conversation.lastStatus != nil {
Text(conversation.lastStatus!.createdAt.relativeFormatted)
if let message = conversation.lastStatus {
Text(message.createdAt.relativeFormatted)
.font(.scaledFootnote)
}
}
@ -91,6 +92,34 @@ struct ConversationsListRow: View {
Label("conversations.action.mark-read", systemImage: "eye")
}
if let message = conversation.lastStatus {
Section("conversations.latest.message") {
Button {
UIPasteboard.general.string = message.content.asRawText
} label: {
Label("status.action.copy-text", systemImage: "doc.on.doc")
}
likeAndBookmark
}
Divider()
if message.account.id != currentAccount.account?.id {
Section(message.reblog?.account.acct ?? message.account.acct) {
Button {
routerPath.presentedSheet = .mentionStatusEditor(account: message.reblog?.account ?? message.account, visibility: .pub)
} label: {
Label("status.action.mention", systemImage: "at")
}
}
Section {
Button(role: .destructive) {
routerPath.presentedSheet = .report(status: message.reblogAsAsStatus ?? message)
} label: {
Label("status.action.report", systemImage: "exclamationmark.bubble")
}
}
}
}
Button(role: .destructive) {
Task {
await viewModel.delete(conversation: conversation)
@ -99,4 +128,24 @@ struct ConversationsListRow: View {
Label("conversations.action.delete", systemImage: "trash")
}
}
@ViewBuilder
private var likeAndBookmark: some View {
Button {
Task {
await viewModel.favorite(conversation: conversation)
}
} label: {
Label(conversation.lastStatus?.favourited ?? false ? "status.action.unfavorite" : "status.action.favorite",
systemImage: conversation.lastStatus?.favourited ?? false ? "star.fill" : "star")
}
Button {
Task {
await viewModel.bookmark(conversation: conversation)
}
} label: {
Label(conversation.lastStatus?.bookmarked ?? false ? "status.action.unbookmark" : "status.action.bookmark",
systemImage: conversation.lastStatus?.bookmarked ?? false ? "bookmark.fill" : "bookmark")
}
}
}

View file

@ -16,29 +16,30 @@ public struct ConversationsListView: View {
public init() {}
private var conversations: [Conversation] {
if viewModel.isLoadingFirstPage {
return Conversation.placeholders()
private var conversations: Binding<[Conversation]> {
if viewModel.isLoadingFirstPage {
return Binding.constant(Conversation.placeholders())
} else {
return $viewModel.conversations
}
}
return viewModel.conversations
}
public var body: some View {
ScrollView {
LazyVStack {
Group {
if !conversations.isEmpty || viewModel.isLoadingFirstPage {
ForEach(conversations) { conversation in
if viewModel.isLoadingFirstPage {
ConversationsListRow(conversation: conversation, viewModel: viewModel)
.padding(.horizontal, .layoutPadding)
.redacted(reason: .placeholder)
} else {
ConversationsListRow(conversation: conversation, viewModel: viewModel)
.padding(.horizontal, .layoutPadding)
}
Divider()
}
ForEach(conversations) { $conversation in
if viewModel.isLoadingFirstPage {
ConversationsListRow(conversation: $conversation, viewModel: viewModel)
.padding(.horizontal, .layoutPadding)
.redacted(reason: .placeholder)
} else {
ConversationsListRow(conversation: $conversation, viewModel: viewModel)
.padding(.horizontal, .layoutPadding)
}
Divider()
}
} else if conversations.isEmpty && !viewModel.isLoadingFirstPage && !viewModel.isError {
EmptyView(iconName: "tray",
title: "conversations.empty.title",

View file

@ -58,13 +58,50 @@ class ConversationsListViewModel: ObservableObject {
await fetchConversations()
}
func favorite(conversation: Conversation) async {
guard let client, let message = conversation.lastStatus else { return }
let endpoint: Endpoint
if message.favourited ?? false {
endpoint = Statuses.unfavorite(id: message.id)
} else {
endpoint = Statuses.favorite(id: message.id)
}
do {
let status: Status = try await client.post(endpoint: endpoint)
updateConversationWithNewLastStatus(conversation: conversation, newLastStatus: status)
} catch {}
}
func bookmark(conversation: Conversation) async {
guard let client, let message = conversation.lastStatus else { return }
let endpoint: Endpoint
if message.bookmarked ?? false {
endpoint = Statuses.unbookmark(id: message.id)
} else {
endpoint = Statuses.bookmark(id: message.id)
}
do {
let status: Status = try await client.post(endpoint: endpoint)
updateConversationWithNewLastStatus(conversation: conversation, newLastStatus: status)
} catch {}
}
private func updateConversationWithNewLastStatus(conversation: Conversation, newLastStatus: Status) {
let newConversation = Conversation(id: conversation.id, unread: conversation.unread, lastStatus: newLastStatus, accounts: conversation.accounts)
updateConversations(conversation: newConversation)
}
private func updateConversations(conversation: Conversation) {
if let index = conversations.firstIndex(where: { $0.id == conversation.id }) {
conversations.remove(at: index)
}
conversations.insert(conversation, at: 0)
conversations = conversations.sorted(by: { ($0.lastStatus?.createdAt.asDate ?? Date.now) > ($1.lastStatus?.createdAt.asDate ?? Date.now) })
}
func handleEvent(event: any StreamEvent) {
if let event = event as? StreamEventConversation {
if let index = conversations.firstIndex(where: { $0.id == event.conversation.id }) {
conversations.remove(at: index)
}
conversations.insert(event.conversation, at: 0)
conversations = conversations.sorted(by: { ($0.lastStatus?.createdAt.asDate ?? Date.now) > ($1.lastStatus?.createdAt.asDate ?? Date.now) })
updateConversations(conversation: event.conversation)
}
}
}

View file

@ -6,6 +6,13 @@ public struct Conversation: Identifiable, Decodable, Hashable, Equatable {
public let lastStatus: Status?
public let accounts: [Account]
public init(id: String, unread: Bool, lastStatus: Status? = nil, accounts: [Account]) {
self.id = id
self.unread = unread
self.lastStatus = lastStatus
self.accounts = accounts
}
public static func placeholder() -> Conversation {
.init(id: UUID().uuidString, unread: false, lastStatus: .placeholder(), accounts: [.placeholder()])
}