Skip to content

Commit 667917b

Browse files
authored
Communication: Improve message actions menu (#355)
* Redesign MessageActionsMenu * Simplify code * Update icons * Adjust spacing
1 parent 3d55de6 commit 667917b

File tree

4 files changed

+180
-100
lines changed

4 files changed

+180
-100
lines changed

ArtemisKit/Sources/Messages/Resources/en.lproj/Localizable.strings

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
"attachment" = "Attachment";
8080

8181
// MARK: Message Actions
82-
"replyInThread" = "Reply in Thread";
82+
"replyInThread" = "Reply";
8383
"copyText" = "Copy Text";
8484
"editMessage" = "Edit Message";
8585
"deleteMessage" = "Delete Message";
@@ -89,8 +89,8 @@
8989
"unmarkAsResolving" = "Doesn't Resolve Post";
9090
"pinMessage" = "Pin Message";
9191
"unpinMessage" = "Unpin Message";
92-
"addBookmark" = "Save Message";
93-
"removeBookmark" = "Remove Bookmark";
92+
"addBookmark" = "Save";
93+
"removeBookmark" = "Remove";
9494
"forwardMessage" = "Forward message";
9595
"forwardMessageShort" = "Forward";
9696

ArtemisKit/Sources/Messages/Views/MessageDetailView/MessageActions/MessageActions.swift

Lines changed: 172 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,42 @@ import SharedModels
1111
import SwiftUI
1212
import UserStore
1313

14-
struct MessageActions: View {
14+
struct MessageActionsBar: View {
1515
@ObservedObject var viewModel: ConversationViewModel
1616
@Binding var message: DataState<BaseMessage>
1717
let conversationPath: ConversationPath?
1818

1919
var body: some View {
20-
Group {
21-
ReplyInThreadButton(viewModel: viewModel, message: $message, conversationPath: conversationPath)
22-
ForwardButton(viewModel: viewModel, message: $message)
20+
MessageActions(viewModel: viewModel, message: $message, conversationPath: conversationPath)
21+
.environment(\.actionsDisplayMode, .bar)
22+
}
23+
}
24+
25+
struct MessageActionsMenu: View {
26+
@ObservedObject var viewModel: ConversationViewModel
27+
@Binding var message: DataState<BaseMessage>
28+
let conversationPath: ConversationPath?
29+
30+
var body: some View {
31+
MessageActions(viewModel: viewModel, message: $message, conversationPath: conversationPath)
32+
.background(.bar, in: .rect(cornerRadius: 10))
33+
.fontWeight(.semibold)
34+
}
35+
}
36+
37+
private struct MessageActions: View {
38+
@ObservedObject var viewModel: ConversationViewModel
39+
@Binding var message: DataState<BaseMessage>
40+
let conversationPath: ConversationPath?
41+
42+
var body: some View {
43+
MenuGroup {
44+
HorizontalMenuGroup {
45+
ReplyButton(viewModel: viewModel, message: $message, conversationPath: conversationPath)
46+
ForwardButton(viewModel: viewModel, message: $message)
47+
BookmarkButton(viewModel: viewModel, message: $message)
48+
}
2349
CopyTextButton(viewModel: viewModel, message: $message)
24-
BookmarkButton(viewModel: viewModel, message: $message)
2550
PinButton(viewModel: viewModel, message: $message)
2651
MarkResolvingButton(viewModel: viewModel, message: $message)
2752
EditDeleteSection(viewModel: viewModel, message: $message)
@@ -30,7 +55,7 @@ struct MessageActions: View {
3055
.font(.title3)
3156
}
3257

33-
struct ReplyInThreadButton: View {
58+
struct ReplyButton: View {
3459
@EnvironmentObject var navigationController: NavigationController
3560
@ObservedObject var viewModel: ConversationViewModel
3661
@Binding var message: DataState<BaseMessage>
@@ -39,7 +64,7 @@ struct MessageActions: View {
3964
var body: some View {
4065
if message.value is Message,
4166
let conversationPath {
42-
Button(R.string.localizable.replyInThread(), systemImage: "text.bubble") {
67+
Button(R.string.localizable.replyInThread(), systemImage: "arrowshape.turn.up.left") {
4368
if let messagePath = MessagePath(
4469
message: $message,
4570
conversationPath: conversationPath,
@@ -64,7 +89,7 @@ struct MessageActions: View {
6489
}
6590

6691
var body: some View {
67-
Button(R.string.localizable.forwardMessageShort(), systemImage: "arrowshape.turn.up.right.fill") {
92+
Button(R.string.localizable.forwardMessageShort(), systemImage: "arrowshape.turn.up.right") {
6893
viewModel.showForwardSheet = true
6994
}
7095
.sheet(isPresented: $viewModel.showForwardSheet) {
@@ -73,7 +98,6 @@ struct MessageActions: View {
7398
ForwardMessageView(viewModel: viewModel)
7499
.font(nil)
75100
}
76-
Divider()
77101
}
78102
}
79103

@@ -119,16 +143,13 @@ struct MessageActions: View {
119143
}
120144

121145
var body: some View {
122-
Group {
123-
Divider()
124-
if message.value?.isBookmarked ?? false {
125-
Button(R.string.localizable.removeBookmark(), systemImage: "bookmark.slash") {
126-
viewModel.toggleBookmark()
127-
}
128-
} else {
129-
Button(R.string.localizable.addBookmark(), systemImage: "bookmark") {
130-
viewModel.toggleBookmark()
131-
}
146+
if message.value?.isBookmarked ?? false {
147+
Button(R.string.localizable.removeBookmark(), systemImage: "bookmark.slash") {
148+
viewModel.toggleBookmark()
149+
}
150+
} else {
151+
Button(R.string.localizable.addBookmark(), systemImage: "bookmark") {
152+
viewModel.toggleBookmark()
132153
}
133154
}
134155
}
@@ -143,33 +164,28 @@ struct MessageActions: View {
143164
}
144165

145166
var body: some View {
146-
Group {
147-
if viewModel.canEdit || viewModel.canDelete {
148-
Divider()
167+
if viewModel.canEdit {
168+
Button(R.string.localizable.editMessage(), systemImage: "pencil") {
169+
viewModel.showEditSheet = true
149170
}
150-
if viewModel.canEdit {
151-
Button(R.string.localizable.editMessage(), systemImage: "pencil") {
152-
viewModel.showEditSheet = true
153-
}
154-
.sheet(isPresented: $viewModel.showEditSheet) {
155-
viewModel.conversationViewModel.selectedMessageId = nil
156-
} content: {
157-
EditMessageView(viewModel: viewModel)
158-
.font(nil)
159-
}
171+
.sheet(isPresented: $viewModel.showEditSheet) {
172+
viewModel.conversationViewModel.selectedMessageId = nil
173+
} content: {
174+
EditMessageView(viewModel: viewModel)
175+
.font(nil)
176+
}
177+
}
178+
if viewModel.canDelete {
179+
Button(R.string.localizable.deleteMessage(), systemImage: "trash", role: .destructive) {
180+
viewModel.showDeleteAlert = true
160181
}
161-
if viewModel.canDelete {
162-
Button(R.string.localizable.deleteMessage(), systemImage: "trash", role: .destructive) {
163-
viewModel.showDeleteAlert = true
182+
.foregroundStyle(.red)
183+
.alert(R.string.localizable.confirmDeletionTitle(), isPresented: $viewModel.showDeleteAlert) {
184+
Button(R.string.localizable.confirm(), role: .destructive) {
185+
viewModel.deleteMessage(navController: navigationController)
164186
}
165-
.foregroundStyle(.red)
166-
.alert(R.string.localizable.confirmDeletionTitle(), isPresented: $viewModel.showDeleteAlert) {
167-
Button(R.string.localizable.confirm(), role: .destructive) {
168-
viewModel.deleteMessage(navController: navigationController)
169-
}
170-
Button(R.string.localizable.cancel(), role: .cancel) {
171-
viewModel.conversationViewModel.selectedMessageId = nil
172-
}
187+
Button(R.string.localizable.cancel(), role: .cancel) {
188+
viewModel.conversationViewModel.selectedMessageId = nil
173189
}
174190
}
175191
}
@@ -186,15 +202,11 @@ struct MessageActions: View {
186202
}
187203

188204
var body: some View {
189-
Group {
190-
if viewModel.canPin {
191-
Divider()
192-
193-
if (message.value as? Message)?.displayPriority == .pinned {
194-
Button(R.string.localizable.unpinMessage(), systemImage: "pin.slash", action: viewModel.togglePinned)
195-
} else {
196-
Button(R.string.localizable.pinMessage(), systemImage: "pin", action: viewModel.togglePinned)
197-
}
205+
if viewModel.canPin {
206+
if (message.value as? Message)?.displayPriority == .pinned {
207+
Button(R.string.localizable.unpinMessage(), systemImage: "pin.slash", action: viewModel.togglePinned)
208+
} else {
209+
Button(R.string.localizable.pinMessage(), systemImage: "pin", action: viewModel.togglePinned)
198210
}
199211
}
200212
}
@@ -224,15 +236,11 @@ struct MessageActions: View {
224236
}
225237

226238
var body: some View {
227-
Group {
228-
if isAbleToMarkResolving {
229-
Divider()
230-
231-
if (message.value as? AnswerMessage)?.resolvesPost ?? false {
232-
Button(R.string.localizable.unmarkAsResolving(), systemImage: "xmark", action: toggleResolved)
233-
} else {
234-
Button(R.string.localizable.markAsResolving(), systemImage: "checkmark", action: toggleResolved)
235-
}
239+
if isAbleToMarkResolving {
240+
if (message.value as? AnswerMessage)?.resolvesPost ?? false {
241+
Button(R.string.localizable.unmarkAsResolving(), systemImage: "xmark", action: toggleResolved)
242+
} else {
243+
Button(R.string.localizable.markAsResolving(), systemImage: "checkmark", action: toggleResolved)
236244
}
237245
}
238246
}
@@ -248,41 +256,45 @@ struct MessageActions: View {
248256
}
249257
}
250258

251-
struct MessageActionsMenu: View {
252-
@ObservedObject var viewModel: ConversationViewModel
253-
@Binding var message: DataState<BaseMessage>
254-
let conversationPath: ConversationPath?
255-
256-
init(viewModel: ConversationViewModel, message: Binding<DataState<BaseMessage>>, conversationPath: ConversationPath?) {
257-
self.viewModel = viewModel
258-
self._message = message
259-
self.conversationPath = conversationPath
260-
}
261-
262-
var body: some View {
263-
VStack {
264-
MessageActions(viewModel: viewModel, message: $message, conversationPath: conversationPath)
259+
private struct MessageActionsStyleModifier: ViewModifier {
260+
@Environment(\.actionsDisplayMode) private var displayMode
261+
262+
func body(content: Content) -> some View {
263+
if displayMode == .menu {
264+
content
265+
.symbolVariant(.fill)
266+
.labelStyle(ContextMenuLabelStyle())
267+
.buttonStyle(.plain)
268+
} else {
269+
content
265270
}
266-
.padding(.vertical, .s)
267-
.background(.bar, in: .rect(cornerRadius: 10))
268-
.fontWeight(.semibold)
269-
.symbolVariant(.fill)
270-
.labelStyle(ContextMenuLabelStyle())
271-
.buttonStyle(.plain)
272-
.loadingIndicator(isLoading: $viewModel.isLoading)
273-
.alert(isPresented: $viewModel.showError, error: viewModel.error, actions: {})
274271
}
275272
}
276273

277274
private struct ContextMenuLabelStyle: LabelStyle {
275+
@Environment(\.actionsDisplayMode) var displayMode
276+
277+
var layout: AnyLayout {
278+
if displayMode == .menuCompact {
279+
AnyLayout(VStackLayout())
280+
} else {
281+
AnyLayout(HStackLayout())
282+
}
283+
}
284+
278285
func makeBody(configuration: Configuration) -> some View {
279-
HStack {
280-
configuration.title
281-
Spacer()
282-
configuration.icon
286+
layout {
287+
if displayMode == .menuCompact {
288+
configuration.icon.frame(height: 40)
289+
configuration.title.font(.callout)
290+
} else {
291+
configuration.title
292+
Spacer()
293+
configuration.icon
294+
}
283295
}
284296
.padding(.horizontal)
285-
.padding(.vertical, .s)
297+
.padding(.vertical, 10)
286298
.contentShape(.rect)
287299
}
288300
}
@@ -303,3 +315,75 @@ extension EnvironmentValues {
303315
}
304316
}
305317
}
318+
319+
// MARK: - Layout
320+
private enum ActionsDisplayMode {
321+
case menu, menuCompact, bar
322+
}
323+
324+
private struct HorizontalMenuGroup<Content: View>: View {
325+
@Environment(\.actionsDisplayMode) private var displayMode
326+
@ViewBuilder var content: Content
327+
328+
var isMenu: Bool { displayMode == .menu }
329+
330+
var body: some View {
331+
HStack(spacing: isMenu ? 0 : .m) {
332+
Group(subviews: content) { subviews in
333+
ForEach(subviews.dropLast()) { subview in
334+
subview
335+
.frame(maxWidth: isMenu ? .infinity : nil)
336+
Divider()
337+
}
338+
subviews.last
339+
.frame(maxWidth: isMenu ? .infinity : nil)
340+
}
341+
}
342+
.fixedSize(horizontal: false, vertical: true)
343+
.environment(\.actionsDisplayMode, isMenu ? .menuCompact : displayMode)
344+
}
345+
}
346+
347+
private struct MenuGroup<Content: View>: View {
348+
@Environment(\.actionsDisplayMode) private var displayMode
349+
@ViewBuilder var content: Content
350+
351+
var layout: AnyLayout {
352+
if displayMode == .menu {
353+
AnyLayout(VStackLayout(spacing: 0))
354+
} else {
355+
AnyLayout(HStackLayout(spacing: .m))
356+
}
357+
}
358+
359+
var body: some View {
360+
layout {
361+
Group(subviews: content) { subviews in
362+
ForEach(subviews.dropLast()) { subview in
363+
subview
364+
Divider()
365+
}
366+
subviews.last
367+
}
368+
}
369+
.fixedSize(horizontal: false, vertical: true)
370+
.modifier(MessageActionsStyleModifier())
371+
}
372+
}
373+
374+
// MARK: Environment+ActionsDisplayMode
375+
376+
private enum ActionsDisplayModeEnvironmentKey: EnvironmentKey {
377+
static let defaultValue: ActionsDisplayMode = .menu
378+
}
379+
380+
private extension EnvironmentValues {
381+
var actionsDisplayMode: ActionsDisplayMode {
382+
get {
383+
self[ActionsDisplayModeEnvironmentKey.self]
384+
}
385+
set {
386+
self[ActionsDisplayModeEnvironmentKey.self] = newValue
387+
}
388+
}
389+
}

ArtemisKit/Sources/Messages/Views/MessageDetailView/MessageCell.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ struct MessageCell: View {
6868
.background(messageBackground,
6969
in: .rect(cornerRadii: viewModel.roundedCorners(isSelected: isSelected)))
7070
.padding(.top, viewModel.isHeaderVisible ? .m : 0)
71-
.padding(.horizontal, useFullWidth ? 0 : (.m + .l) / 2)
71+
.padding(.horizontal, useFullWidth ? 0 : .m)
7272
.opacity(opacity)
7373
.id(message.value?.id.description)
7474
}

ArtemisKit/Sources/Messages/Views/MessageDetailView/MessageDetailView.swift

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -145,14 +145,10 @@ private extension MessageDetailView {
145145

146146
// Only display labels if we have enough space
147147
ViewThatFits(in: .horizontal) {
148-
HStack {
149-
MessageActions(viewModel: viewModel, message: $message, conversationPath: nil)
150-
}
151-
HStack(spacing: .l) {
152-
MessageActions(viewModel: viewModel, message: $message, conversationPath: nil)
153-
.labelStyle(.iconOnly)
154-
.fontWeight(.bold)
155-
}
148+
MessageActionsBar(viewModel: viewModel, message: $message, conversationPath: nil)
149+
MessageActionsBar(viewModel: viewModel, message: $message, conversationPath: nil)
150+
.labelStyle(.iconOnly)
151+
.fontWeight(.bold)
156152
}
157153
.loadingIndicator(isLoading: $viewModel.isLoading)
158154
}

0 commit comments

Comments
 (0)