Skip to content

Commit

Permalink
Communication: Fix message (re)actions menu being offset (#196)
Browse files Browse the repository at this point in the history
  • Loading branch information
anian03 authored Oct 26, 2024
1 parent 6abf231 commit f93122c
Show file tree
Hide file tree
Showing 5 changed files with 53 additions and 94 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ class ConversationViewModel: BaseViewModel {

@Published var isConversationInfoSheetPresented = false
@Published var selectedMessageId: Int64?
var isPerformingMessageAction = false

var isAllowedToPost: Bool {
guard let channel = conversation.baseConversation as? Channel else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class ReactionsViewModel {
self.messageBinding.wrappedValue = .done(response: response)
}
}
conversationViewModel.selectedMessageId = nil
}

func isMyReaction(_ emoji: String) -> Bool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ struct MessageActions: View {
var body: some View {
Group {
ReplyInThreadButton(viewModel: viewModel, message: $message, conversationPath: conversationPath)
CopyTextButton(message: $message)
CopyTextButton(viewModel: viewModel, message: $message)
PinButton(viewModel: viewModel, message: $message)
MarkResolvingButton(viewModel: viewModel, message: $message)
EditDeleteSection(viewModel: viewModel, message: $message)
Expand All @@ -46,6 +46,7 @@ struct MessageActions: View {
conversationViewModel: viewModel
) {
navigationController.tabPath.append(messagePath)
viewModel.selectedMessageId = nil
} else {
viewModel.presentError(userFacingError: UserFacingError(title: R.string.localizable.detailViewCantBeOpened()))
}
Expand All @@ -56,6 +57,7 @@ struct MessageActions: View {

struct CopyTextButton: View {
@EnvironmentObject var navController: NavigationController
@ObservedObject var viewModel: ConversationViewModel
@Binding var message: DataState<BaseMessage>
@State private var showSuccess = false

Expand All @@ -65,6 +67,7 @@ struct MessageActions: View {
if !navController.tabPath.isEmpty && message.value is Message {
showSuccess = true
}
viewModel.selectedMessageId = nil
}
.opacity(showSuccess ? 0 : 1)
.overlay {
Expand Down Expand Up @@ -117,19 +120,16 @@ struct MessageActions: View {
Divider()

Button(R.string.localizable.editMessage(), systemImage: "pencil") {
viewModel.isPerformingMessageAction = true
showEditSheet = true
}
.sheet(isPresented: $showEditSheet) {
viewModel.isPerformingMessageAction = false
viewModel.selectedMessageId = nil
} content: {
editMessage
.font(nil)
}

Button(R.string.localizable.deleteMessage(), systemImage: "trash", role: .destructive) {
viewModel.isPerformingMessageAction = true
showDeleteAlert = true
}
.alert(R.string.localizable.confirmDeletionTitle(), isPresented: $showDeleteAlert) {
Expand All @@ -144,18 +144,16 @@ struct MessageActions: View {
success = await viewModel.deleteMessage(messageId: message.value?.id)
}
viewModel.isLoading = false
viewModel.isPerformingMessageAction = false
viewModel.selectedMessageId = nil
if success {
// if we deleted a Message and are in the MessageDetailView we pop it
if navigationController.outerPath.count == 3 && tempMessage is Message {
navigationController.outerPath.removeLast()
if !navigationController.tabPath.isEmpty && tempMessage is Message {
navigationController.tabPath.removeLast()
}
}
}
}
Button(R.string.localizable.cancel(), role: .cancel) {
viewModel.isPerformingMessageAction = false
viewModel.selectedMessageId = nil
}
}
Expand Down Expand Up @@ -244,14 +242,12 @@ struct MessageActions: View {

func togglePinned() {
guard let message = message.value as? Message else { return }
viewModel.isPerformingMessageAction = true
Task {
let result = await viewModel.togglePinned(for: message)
let oldRole = message.authorRole
if var newMessageResult = result.value as? Message {
newMessageResult.authorRole = oldRole
self.$message.wrappedValue = .done(response: newMessageResult)
viewModel.isPerformingMessageAction = false
viewModel.selectedMessageId = nil
}
}
Expand Down Expand Up @@ -297,10 +293,8 @@ struct MessageActions: View {

func toggleResolved() {
guard let message = message.value as? AnswerMessage else { return }
viewModel.isPerformingMessageAction = true
Task {
if await viewModel.toggleResolving(for: message) {
viewModel.isPerformingMessageAction = false
viewModel.selectedMessageId = nil
}
}
Expand All @@ -320,18 +314,29 @@ struct MessageReactionsPopover: View {
}

var body: some View {
HStack(spacing: .m) {
EmojiTextButton(viewModel: reactionsViewModel, emoji: "😂")
EmojiTextButton(viewModel: reactionsViewModel, emoji: "👍")
EmojiTextButton(viewModel: reactionsViewModel, emoji: "")
EmojiTextButton(viewModel: reactionsViewModel, emoji: "🚀")
EmojiPickerButton(viewModel: reactionsViewModel)
HStack(alignment: .center) {
HStack(spacing: .m) {
EmojiTextButton(viewModel: reactionsViewModel, emoji: "😂")
EmojiTextButton(viewModel: reactionsViewModel, emoji: "👍")
EmojiTextButton(viewModel: reactionsViewModel, emoji: "")
EmojiTextButton(viewModel: reactionsViewModel, emoji: "🚀")
EmojiPickerButton(viewModel: reactionsViewModel)
}
.padding(.m)
.buttonStyle(.plain)
.font(.headline)
.symbolVariant(.fill)
.alert(isPresented: $viewModel.showError, error: viewModel.error, actions: {})
.background(.bar, in: .rect(cornerRadius: 10))

Button {
viewModel.selectedMessageId = nil
} label: {
Image(systemName: "xmark.circle")
}
.font(.title2)
.frame(maxWidth: .infinity, alignment: .center)
}
.padding(.l)
.buttonStyle(.plain)
.font(.headline)
.symbolVariant(.fill)
.alert(isPresented: $viewModel.showError, error: viewModel.error, actions: {})
}
}

Expand Down Expand Up @@ -376,8 +381,6 @@ private struct ContextMenuLabelStyle: LabelStyle {

private struct EmojiTextButton: View {

@Environment(\.dismiss) var dismiss

var viewModel: ReactionsViewModel

let emoji: String
Expand All @@ -386,7 +389,7 @@ private struct EmojiTextButton: View {
Text("\(emoji)")
.font(.title3)
.foregroundColor(Color.Artemis.primaryLabel)
.frame(width: .mediumImage, height: .mediumImage)
.frame(width: .mediumImage * 0.75, height: .mediumImage * 0.75)
.padding(.s)
.background(
Capsule().fill(Color.Artemis.reactionCapsuleColor)
Expand All @@ -395,7 +398,6 @@ private struct EmojiTextButton: View {
if let emojiId = Smile.alias(emoji: emoji) {
Task {
await viewModel.addReaction(emojiId: emojiId)
dismiss()
}
}
}
Expand All @@ -404,8 +406,6 @@ private struct EmojiTextButton: View {

private struct EmojiPickerButton: View {

@Environment(\.dismiss) var dismiss

var viewModel: ReactionsViewModel

@State private var showEmojiPicker = false
Expand All @@ -420,8 +420,8 @@ private struct EmojiPickerButton: View {
.resizable()
.scaledToFit()
.foregroundColor(Color.Artemis.secondaryLabel)
.frame(width: .smallImage, height: .smallImage)
.padding(.l)
.frame(width: .smallImage * 0.75, height: .smallImage * 0.75)
.padding(.m * 1.5)
.background(Capsule().fill(Color.Artemis.reactionCapsuleColor))
}
.sheet(isPresented: $showEmojiPicker) {
Expand All @@ -437,7 +437,6 @@ private struct EmojiPickerButton: View {
Task {
await viewModel.addReaction(emojiId: emojiId)
selectedEmoji = nil
dismiss()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ struct MessageCell: View {

var body: some View {
VStack(alignment: .leading, spacing: .s) {
reactionMenuIfAvailable

HStack {
VStack(alignment: .leading, spacing: .s) {
pinnedIndicator
Expand Down Expand Up @@ -57,15 +59,9 @@ struct MessageCell: View {
.background(backgroundOnPress, in: .rect(cornerRadius: .m))
.background(messageBackground,
in: .rect(cornerRadii: viewModel.roundedCorners(isSelected: isSelected)))
.modifier(ReactionsPopoverModifier(isSelected: isSelected,
viewModel: viewModel,
conversationViewModel: conversationViewModel,
message: $message))
.padding(.top, viewModel.isHeaderVisible ? .m : 0)
.padding(.horizontal, useFullWidth ? 0 : (.m + .l) / 2)
.opacity(opacity)
/// Ensure the message is fully visible when selected, space for reactions popover
.padding(.top, isSelected ? 100 : 0)
.id(message.value?.id.description)
}
}
Expand Down Expand Up @@ -256,7 +252,7 @@ private extension MessageCell {
}

@ViewBuilder var actionsMenuIfAvailable: some View {
if isSelected {
if isSelected && !useFullWidth {
MessageActionsMenu(viewModel: conversationViewModel,
message: $message,
conversationPath: viewModel.conversationPath)
Expand All @@ -266,6 +262,17 @@ private extension MessageCell {
}
}

@ViewBuilder var reactionMenuIfAvailable: some View {
if isSelected {
MessageReactionsPopover(viewModel: conversationViewModel,
message: $message,
conversationPath: viewModel.conversationPath)
.frame(maxWidth: .infinity)
.padding(.bottom, 5)
.transition(.scale(0, anchor: .bottom).combined(with: .opacity))
}
}

func openThread(showErrorOnFailure: Bool = true, presentKeyboard: Bool = false) {
// We cannot navigate to details if conversation path is nil, e.g. in the message detail view.
if let conversationPath = viewModel.conversationPath,
Expand All @@ -284,8 +291,10 @@ private extension MessageCell {
// MARK: Gestures

func onTapPresentMessage() {
guard conversationViewModel.selectedMessageId == nil else { return }
openThread(showErrorOnFailure: false)
if conversationViewModel.selectedMessageId == nil || isSelected {
openThread(showErrorOnFailure: false)
}
conversationViewModel.selectedMessageId = nil
}

func onSwipePresentMessage() {
Expand All @@ -299,14 +308,8 @@ private extension MessageCell {

let feedback = UIImpactFeedbackGenerator(style: .heavy)
feedback.impactOccurred()
if useFullWidth {
viewModel.showReactionsPopover = true
} else {
withAnimation {
conversationViewModel.selectedMessageId = message.value?.id
} completion: {
viewModel.showReactionsPopover = true
}
withAnimation {
conversationViewModel.selectedMessageId = message.value?.id
}
viewModel.isDetectingLongPress = false
}
Expand Down Expand Up @@ -368,49 +371,6 @@ private extension MessageCell {
}
}

// MARK: - ReactionsPopover
struct ReactionsPopoverModifier: ViewModifier {
@Environment(\.messageUseFullWidth) var useFullWidth
let isSelected: Bool
let viewModel: MessageCellModel
let conversationViewModel: ConversationViewModel
@Binding var message: DataState<BaseMessage>

func body(content: Content) -> some View {
content
.popover(
isPresented: Binding(get: {
(isSelected || useFullWidth)
&& viewModel.showReactionsPopover
}, set: { value, _ in
if !value {
if !conversationViewModel.isPerformingMessageAction {
conversationViewModel.selectedMessageId = nil
}
viewModel.showReactionsPopover = false
}
}),
attachmentAnchor: .point(useFullWidth ? .bottom : .top),
arrowEdge: useFullWidth ? .top : .bottom
) {
MessageReactionsPopover(
viewModel: conversationViewModel,
message: $message,
conversationPath: viewModel.conversationPath
)
.presentationCompactAdaptation(.popover)
.presentationBackgroundInteraction(.enabled)
.presentationBackground(.bar)
}
.onChange(of: conversationViewModel.selectedMessageId) {
// Reset recations popover presentation when message is no longer selected
if conversationViewModel.selectedMessageId == nil && viewModel.showReactionsPopover {
viewModel.showReactionsPopover = false
}
}
}
}

// MARK: - Environment+IsMessageOffline

private enum IsMessageOfflineEnvironmentKey: EnvironmentKey {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ struct MessageDetailView: View {
top(message: message)
answers(of: message, proxy: proxy)
}
.defaultScrollAnchor(.bottom)
}
if !((viewModel.conversation.baseConversation as? Channel)?.isArchived ?? false),
let message = message as? Message {
Expand Down Expand Up @@ -92,6 +91,7 @@ private extension MessageDetailView {
)
.environment(\.isEmojiPickerButtonVisible, true)
.environment(\.messageUseFullWidth, true)
.animation(.default, value: viewModel.selectedMessageId)
}

@ViewBuilder var divider: some View {
Expand Down

0 comments on commit f93122c

Please sign in to comment.