Highlight selected emoji in picker, correct add more button in menu, fix order of count label on outgoing messages (#1430)

- Highlight selected emoji in picker
- Correct add more button in menu
- Fix the order of count/key label on outgoing messages
- Fix the size padding of emoji in reactions
This commit is contained in:
David Langley
2023-08-01 08:59:10 +01:00
committed by GitHub
parent 4fc5b6fb15
commit 686fee617e
17 changed files with 86 additions and 55 deletions
@@ -136,9 +136,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case (.dismissMediaUploadPreview, .mediaUploadPreview(let roomID, _)):
return .room(roomID: roomID)
case (.presentEmojiPicker(let itemID), .room(let roomID)):
return .emojiPicker(roomID: roomID, itemID: itemID)
case (.dismissEmojiPicker, .emojiPicker(let roomID, _)):
case (.presentEmojiPicker(let itemID, let selectedEmoji), .room(let roomID)):
return .emojiPicker(roomID: roomID, itemID: itemID, selectedEmojis: selectedEmoji)
case (.dismissEmojiPicker, .emojiPicker(let roomID, _, _)):
return .room(roomID: roomID)
case (.presentMessageForwarding(let itemID), .room(let roomID)):
@@ -197,8 +197,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case (.mediaUploadPreview, .dismissMediaUploadPreview, .room):
break
case (.room, .presentEmojiPicker, .emojiPicker(_, let itemID)):
presentEmojiPicker(for: itemID)
case (.room, .presentEmojiPicker, .emojiPicker(_, let itemID, let selectedEmoji)):
presentEmojiPicker(for: itemID, selectedEmoji: selectedEmoji)
case (.emojiPicker, .dismissEmojiPicker, .room):
break
@@ -303,8 +303,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
stateMachine.tryEvent(.presentMediaUploadPicker(source: source))
case .presentMediaUploadPreviewScreen(let url):
stateMachine.tryEvent(.presentMediaUploadPreview(fileURL: url))
case .presentEmojiPicker(let itemID):
stateMachine.tryEvent(.presentEmojiPicker(itemID: itemID))
case .presentEmojiPicker(let itemID, let selectedEmojis):
stateMachine.tryEvent(.presentEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis))
case .presentLocationPicker:
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .picker))
case .presentLocationViewer(_, let geoURI, let description):
@@ -480,9 +480,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
}
}
private func presentEmojiPicker(for itemID: TimelineItemIdentifier) {
private func presentEmojiPicker(for itemID: TimelineItemIdentifier, selectedEmoji: Set<String>) {
let params = EmojiPickerScreenCoordinatorParameters(emojiProvider: emojiProvider,
itemID: itemID)
itemID: itemID, selectedEmojis: selectedEmoji)
let coordinator = EmojiPickerScreenCoordinator(parameters: params)
coordinator.callback = { [weak self] action in
switch action {
@@ -632,7 +632,7 @@ private extension RoomFlowCoordinator {
case roomDetails(roomID: String, isRoot: Bool)
case mediaUploadPicker(roomID: String, source: MediaPickerScreenSource)
case mediaUploadPreview(roomID: String, fileURL: URL)
case emojiPicker(roomID: String, itemID: TimelineItemIdentifier)
case emojiPicker(roomID: String, itemID: TimelineItemIdentifier, selectedEmojis: Set<String>)
case mapNavigator(roomID: String)
case roomMemberDetails(roomID: String, member: HashableRoomMemberWrapper)
case messageForwarding(roomID: String, itemID: TimelineItemIdentifier)
@@ -659,7 +659,7 @@ private extension RoomFlowCoordinator {
case presentMediaUploadPreview(fileURL: URL)
case dismissMediaUploadPreview
case presentEmojiPicker(itemID: TimelineItemIdentifier)
case presentEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>)
case dismissEmojiPicker
case presentMapNavigator(interactionMode: StaticLocationInteractionMode)
@@ -19,6 +19,7 @@ import SwiftUI
struct EmojiPickerScreenCoordinatorParameters {
let emojiProvider: EmojiProviderProtocol
let itemID: TimelineItemIdentifier
let selectedEmojis: Set<String>
}
enum EmojiPickerScreenCoordinatorAction {
@@ -52,6 +53,6 @@ final class EmojiPickerScreenCoordinator: CoordinatorProtocol {
}
func toPresentable() -> AnyView {
AnyView(EmojiPickerScreen(context: viewModel.context))
AnyView(EmojiPickerScreen(context: viewModel.context, selectedEmojis: parameters.selectedEmojis))
}
}
@@ -20,6 +20,7 @@ import SwiftUI
struct EmojiPickerScreen: View {
@ObservedObject var context: EmojiPickerScreenViewModel.Context
var selectedEmojis = Set<String>()
@State var searchString = ""
@State private var isSearching = false
@@ -39,7 +40,10 @@ struct EmojiPickerScreen: View {
context.send(viewAction: .emojiTapped(emoji: emoji))
} label: {
Text(emoji.value)
.padding(9.0)
.font(.compound.headingXL)
.background(Circle()
.foregroundColor(emojiBackgroundColor(for: emoji.value)))
}
}
} header: {
@@ -65,6 +69,14 @@ struct EmojiPickerScreen: View {
}
}
private func emojiBackgroundColor(for emoji: String) -> Color {
if selectedEmojis.contains(emoji) {
return .compound.bgActionPrimaryRest
} else {
return .clear
}
}
@ToolbarContentBuilder
var toolbar: some ToolbarContent {
ToolbarItem(placement: .cancellationAction) {
@@ -92,12 +104,12 @@ struct EmojiPickerScreen_Previews: PreviewProvider {
static let viewModel = EmojiPickerScreenViewModel(emojiProvider: EmojiProvider())
static var previews: some View {
EmojiPickerScreen(context: viewModel.context)
EmojiPickerScreen(context: viewModel.context, selectedEmojis: ["😀", "😄"])
.previewDisplayName("Screen")
Text("Timeline view")
.sheet(isPresented: .constant(true)) {
EmojiPickerScreen(context: viewModel.context)
EmojiPickerScreen(context: viewModel.context, selectedEmojis: ["😀", "😄"])
}
.previewDisplayName("Sheet")
}
@@ -31,7 +31,7 @@ enum RoomScreenCoordinatorAction {
case presentRoomDetails
case presentLocationPicker
case presentLocationViewer(body: String, geoURI: GeoURI, description: String?)
case presentEmojiPicker(itemID: TimelineItemIdentifier)
case presentEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>)
case presentRoomMemberDetails(member: RoomMemberProxyProtocol)
case presentMessageForwarding(itemID: TimelineItemIdentifier)
}
@@ -67,8 +67,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
switch action {
case .displayRoomDetails:
actionsSubject.send(.presentRoomDetails)
case .displayEmojiPicker(let itemID):
actionsSubject.send(.presentEmojiPicker(itemID: itemID))
case .displayEmojiPicker(let itemID, let selectedEmojis):
actionsSubject.send(.presentEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis))
case .displayReportContent(let itemID, let senderID):
actionsSubject.send(.presentReportContent(itemID: itemID, senderID: senderID))
case .displayCameraPicker:
@@ -22,7 +22,7 @@ import OrderedCollections
enum RoomScreenViewModelAction {
case displayRoomDetails
case displayEmojiPicker(itemID: TimelineItemIdentifier)
case displayEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>)
case displayReportContent(itemID: TimelineItemIdentifier, senderID: String)
case displayCameraPicker
case displayMediaPicker
@@ -728,8 +728,13 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
// MARK: - Reactions
private func showEmojiPicker(for itemID: TimelineItemIdentifier) {
guard let item = state.timelineViewState.itemsDictionary[itemID.timelineID], item.isReactable else { return }
callback?(.displayEmojiPicker(itemID: itemID))
guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemID }),
timelineItem.isReactable,
let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else {
return
}
let selectedEmojis = Set(eventTimelineItem.properties.reactions.compactMap { $0.isHighlighted ? $0.key : nil })
callback?(.displayEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis))
}
private func showReactionSummary(for itemID: TimelineItemIdentifier, selectedKey: String) {
@@ -20,7 +20,6 @@ import SwiftUI
struct TimelineItemBubbledStylerView<Content: View>: View {
@EnvironmentObject private var context: RoomScreenViewModel.Context
@Environment(\.timelineGroupStyle) private var timelineGroupStyle
@Environment(\.layoutDirection) var layoutDirection: LayoutDirection
let timelineItem: EventBasedTimelineItemProtocol
@ViewBuilder let content: () -> Content
@@ -41,11 +40,6 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
return 8
}
var reactionsLayoutDirection: LayoutDirection {
guard timelineItem.isOutgoing else { return layoutDirection }
return layoutDirection == .leftToRight ? .rightToLeft : .leftToRight
}
var body: some View {
ZStack(alignment: .trailingFirstTextBaseline) {
VStack(alignment: alignment, spacing: -12) {
@@ -109,8 +103,8 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
if !timelineItem.properties.reactions.isEmpty {
TimelineReactionsView(itemID: timelineItem.id,
reactions: timelineItem.properties.reactions,
isLayoutRTL: timelineItem.isOutgoing,
collapsed: context.reactionsCollapsedBinding(for: timelineItem.id))
.environment(\.layoutDirection, reactionsLayoutDirection)
// Workaround to stop the message long press stealing the touch from the reaction buttons
.onTapGesture { }
}
@@ -22,11 +22,16 @@ struct TimelineReactionsView: View {
private let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)
@EnvironmentObject private var context: RoomScreenViewModel.Context
@Environment(\.layoutDirection) private var layoutDirection: LayoutDirection
@Namespace private var animation
let itemID: TimelineItemIdentifier
let reactions: [AggregatedReaction]
var isLayoutRTL = false
@Binding var collapsed: Bool
var reactionsLayoutDirection: LayoutDirection {
guard isLayoutRTL else { return layoutDirection }
return layoutDirection == .leftToRight ? .rightToLeft : .leftToRight
}
var body: some View {
CollapsibleReactionLayout(itemSpacing: 4, rowSpacing: 4, collapsed: collapsed, rowsBeforeCollapsible: 2) {
@@ -38,6 +43,7 @@ struct TimelineReactionsView: View {
context.send(viewAction: .reactionSummary(itemID: itemID, key: key))
}
.reactionLayoutItem(.reaction)
.environment(\.layoutDirection, layoutDirection)
}
Button {
collapsed.toggle()
@@ -46,6 +52,7 @@ struct TimelineReactionsView: View {
.transaction { $0.animation = nil }
}
.reactionLayoutItem(.expandCollapse)
.environment(\.layoutDirection, layoutDirection)
Button {
context.send(viewAction: .displayEmojiPicker(itemID: itemID))
} label: {
@@ -53,6 +60,7 @@ struct TimelineReactionsView: View {
}
.reactionLayoutItem(.addMore)
}
.environment(\.layoutDirection, reactionsLayoutDirection)
.animation(.easeInOut(duration: 0.1), value: reactions)
.padding(.leading, 4)
}
@@ -86,10 +94,12 @@ struct TimelineReactionButtonLabel<Content: View>: View {
struct TimelineCollapseButtonLabel: View {
var collapsed: Bool
@ScaledMetric(relativeTo: .subheadline) private var lineHeight = 20
var body: some View {
TimelineReactionButtonLabel {
Text(collapsed ? L10n.screenRoomReactionsShowMore : L10n.screenRoomReactionsShowLess)
.frame(height: lineHeight, alignment: .center)
.padding(.vertical, 6)
.padding(.horizontal, 12)
.font(.compound.bodyMD)
@@ -102,6 +112,7 @@ struct TimelineReactionButton: View {
let reaction: AggregatedReaction
let toggleReaction: (String) -> Void
let showReactionSummary: (String) -> Void
@ScaledMetric(relativeTo: .subheadline) private var lineHeight = 20
var body: some View {
label
@@ -116,14 +127,18 @@ struct TimelineReactionButton: View {
var label: some View {
TimelineReactionButtonLabel(isHighlighted: reaction.isHighlighted) {
HStack(spacing: 4) {
// Designs have bodyMD for the key but practically this makes
// emojis too big. bodySM gives a more appropriate size when compared
// to the count text and the lineHeight/padding in the designs.
Text(reaction.displayKey)
.font(.compound.bodyMD)
.font(.compound.bodySM)
if reaction.count > 1 {
Text(String(reaction.count))
.font(.compound.bodyMD)
.foregroundColor(textColor)
}
}
.frame(height: lineHeight, alignment: .center)
.padding(.vertical, 6)
.padding(.horizontal, 12)
}
@@ -157,16 +172,16 @@ struct TimelineReactionViewPreviewsContainer: View {
var body: some View {
VStack {
TimelineReactionsView(itemID: .init(timelineID: "1"), reactions: [
AggregatedReaction.mockReactionWithLongText,
AggregatedReaction.mockReactionWithLongTextRTL
], collapsed: .constant(true))
TimelineReactionsView(itemID: .init(timelineID: "1"),
reactions: [AggregatedReaction.mockReactionWithLongText,
AggregatedReaction.mockReactionWithLongTextRTL],
collapsed: .constant(true))
Divider()
TimelineReactionsView(itemID: .init(timelineID: "2"), reactions: Array(AggregatedReaction.mockReactions.prefix(3)), collapsed: .constant(true))
Divider()
TimelineReactionsView(itemID: .init(timelineID: "3"), reactions: AggregatedReaction.mockReactions, collapsed: $collapseState1)
Divider()
TimelineReactionsView(itemID: .init(timelineID: "4"), reactions: AggregatedReaction.mockReactions, collapsed: $collapseState2)
.environment(\.layoutDirection, .rightToLeft)
TimelineReactionsView(itemID: .init(timelineID: "4"), reactions: AggregatedReaction.mockReactions, isLayoutRTL: true, collapsed: $collapseState2)
}
.background(Color.red)
.frame(maxWidth: 250, alignment: .leading)
@@ -121,6 +121,7 @@ extension RoomTimelineItemProtocol {
public struct TimelineItemMenu: View {
@EnvironmentObject private var context: RoomScreenViewModel.Context
@Environment(\.dismiss) private var dismiss
@ScaledMetric private var addMoreButtonIconSize = 24
let item: EventBasedTimelineItemProtocol
let actions: TimelineItemMenuActions
@@ -209,8 +210,12 @@ public struct TimelineItemMenu: View {
context.send(viewAction: .displayEmojiPicker(itemID: item.id))
}
} label: {
Image(systemName: "plus.circle")
.font(.compound.headingLG)
Image(asset: Asset.Images.timelineReactionAddMore)
.resizable()
.frame(width: addMoreButtonIconSize, height: addMoreButtonIconSize)
.frame(maxHeight: .infinity, alignment: .center)
.foregroundColor(.compound.iconSecondary)
.padding(10)
}
}
.padding(.horizontal)
@@ -224,11 +229,10 @@ public struct TimelineItemMenu: View {
context.send(viewAction: .toggleReaction(key: emoji, itemID: item.id))
} label: {
Text(emoji)
.padding(8.0)
.padding(8)
.font(.compound.headingLG)
.background(Circle()
.foregroundColor(reactionBackgroundColor(for: emoji)))
Spacer()
}
}