Skip to content

Commit

Permalink
Fixes /issues/114 - Replying to timeline items
Browse files Browse the repository at this point in the history
  • Loading branch information
stefanceriu authored Sep 14, 2022
1 parent dd23857 commit ffee192
Show file tree
Hide file tree
Showing 18 changed files with 229 additions and 79 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

"room_timeline_permalink_creation_failure" = "Failed creating the permalink";

"room_timeline_replying_to" = "Replying to %@";

// MARK: - Authentication

"authentication_login_title" = "Welcome back!";
Expand Down
4 changes: 4 additions & 0 deletions ElementX/Sources/Generated/Strings+Untranslated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ extension ElementL10n {
public static let authenticationServerInfoTitle = ElementL10n.tr("Untranslated", "authentication_server_info_title")
/// Failed creating the permalink
public static let roomTimelinePermalinkCreationFailure = ElementL10n.tr("Untranslated", "room_timeline_permalink_creation_failure")
/// Replying to %@
public static func roomTimelineReplyingTo(_ p1: Any) -> String {
return ElementL10n.tr("Untranslated", "room_timeline_replying_to", String(describing: p1))
}
/// Bubbled Timeline
public static let roomTimelineStyleBubbledLongDescription = ElementL10n.tr("Untranslated", "room_timeline_style_bubbled_long_description")
/// Plain Timeline
Expand Down
11 changes: 11 additions & 0 deletions ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ enum TimelineItemContextMenuAction: Hashable {
case quote
case copyPermalink
case redact
case reply
}

enum RoomScreenComposerMode: Equatable {
case `default`
case reply(id: String, displayName: String)
}

enum RoomScreenViewAction {
Expand All @@ -33,6 +39,7 @@ enum RoomScreenViewAction {
case linkClicked(url: URL)
case sendMessage
case sendReaction(key: String, eventID: String)
case cancelReply
}

struct RoomScreenViewState: BindableState {
Expand All @@ -45,13 +52,17 @@ struct RoomScreenViewState: BindableState {

var contextMenuBuilder: (@MainActor (_ itemId: String) -> TimelineItemContextMenu)?

var composerMode: RoomScreenComposerMode = .default

var messageComposerDisabled = false // Remove this when we have local echoes
var sendButtonDisabled: Bool {
bindings.composerText.count == 0
}
}

struct RoomScreenViewStateBindings {
var composerText: String
var composerFocused: Bool

/// Information describing the currently displayed alert.
var alertInfo: AlertInfo<RoomScreenErrorType>?
Expand Down
61 changes: 44 additions & 17 deletions ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
super.init(initialViewState: RoomScreenViewState(roomTitle: roomName ?? "Unknown room 💥",
roomAvatar: roomAvatar,
roomEncryptionBadge: roomEncryptionBadge,
bindings: .init(composerText: "")))
bindings: .init(composerText: "", composerFocused: false)))

timelineController.callbacks
.receive(on: DispatchQueue.main)
Expand Down Expand Up @@ -75,23 +75,19 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
default:
state.isBackPaginating = false
}

case .itemAppeared(let id):
await timelineController.processItemAppearance(id)
case .itemDisappeared(let id):
await timelineController.processItemDisappearance(id)
case .linkClicked(let url):
MXLog.warning("Link clicked: \(url)")
case .sendMessage:
guard state.bindings.composerText.count > 0 else {
return
}

await timelineController.sendMessage(state.bindings.composerText)
state.bindings.composerText = ""
await sendCurrentMessage()
case .sendReaction(let key, _):
#warning("Reaction implementation awaiting SDK support.")
MXLog.warning("React with \(key) failed. Not implemented.")
case .cancelReply:
state.composerMode = .default
}
}

Expand All @@ -105,6 +101,35 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
state.items = stateItems
}

private func sendCurrentMessage() async {
guard !state.bindings.composerText.isEmpty else {
fatalError("This message should never be empty")
}

state.messageComposerDisabled = true

switch state.composerMode {
case .reply(let itemId, _):
await timelineController.sendReply(state.bindings.composerText, to: itemId)
default:
await timelineController.sendMessage(state.bindings.composerText)
}

state.bindings.composerText = ""
state.composerMode = .default

state.messageComposerDisabled = false
}

private func displayError(_ type: RoomScreenErrorType) {
switch type {
case .alert(let message):
state.bindings.alertInfo = AlertInfo(id: type,
title: ElementL10n.dialogTitleError,
message: message)
}
}

// MARK: ContextMenus

private func buildContexMenuForItemId(_ itemId: String) -> TimelineItemContextMenu {
Expand All @@ -120,7 +145,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
}

let actions: [TimelineItemContextMenuAction] = [
.copy, .quote, .copyPermalink
.copy, .quote, .copyPermalink, .reply
]

#warning("Outgoing actions to be handled with the new Timeline API.")
Expand All @@ -141,6 +166,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
case .copy:
UIPasteboard.general.string = item.text
case .quote:
state.bindings.composerFocused = true
state.bindings.composerText = "> \(item.text)"
case .copyPermalink:
do {
Expand All @@ -151,15 +177,16 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
}
case .redact:
redact(itemId)
case .reply:
state.bindings.composerFocused = true
state.composerMode = .reply(id: item.id, displayName: item.senderDisplayName ?? item.senderId)
}
}

private func displayError(_ type: RoomScreenErrorType) {
switch type {
case .alert(let message):
state.bindings.alertInfo = AlertInfo(id: type,
title: ElementL10n.dialogTitleError,
message: message)

switch action {
case .reply:
break
default:
state.composerMode = .default
}
}

Expand Down
81 changes: 71 additions & 10 deletions ElementX/Sources/Screens/RoomScreen/View/MessageComposer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,77 @@ import SwiftUI

struct MessageComposer: View {
@Binding var text: String
var disabled: Bool
let action: () -> Void
@Binding var focused: Bool
let sendingDisabled: Bool
let type: RoomScreenComposerMode

let sendAction: () -> Void
let replyCancellationAction: () -> Void

var body: some View {
HStack(alignment: .bottom) {
MessageComposerTextField(placeholder: "Send a message", text: $text, maxHeight: 300)
let rect = RoundedRectangle(cornerRadius: 8.0)
VStack(alignment: .leading, spacing: 2.0) {
if case let .reply(_, displayName) = type {
MessageComposerReplyHeader(displayName: displayName, action: replyCancellationAction)
}
MessageComposerTextField(placeholder: "Send a message",
text: $text,
focused: $focused,
maxHeight: 300)
}
.padding(4.0)
.frame(minHeight: 44.0)
.clipShape(rect)
.overlay(rect.stroke(borderColor, lineWidth: borderWidth))
.animation(.elementDefault, value: type)
.animation(.elementDefault, value: borderWidth)

Button {
action()
sendAction()
} label: {
Image(uiImage: Asset.Images.timelineComposerSendMessage.image)
.background(Circle()
.foregroundColor(.global.white)
.padding(2)
)
}
.padding(.bottom, 6.0)
.disabled(disabled)
.opacity(disabled ? 0.5 : 1.0)
.animation(.elementDefault, value: disabled)
.disabled(sendingDisabled)
.opacity(sendingDisabled ? 0.5 : 1.0)
.animation(.elementDefault, value: sendingDisabled)
.keyboardShortcut(.return, modifiers: [.command])
}
}

private var borderColor: Color {
.element.accent
}

private var borderWidth: CGFloat {
focused ? 2.0 : 1.0
}
}

private struct MessageComposerReplyHeader: View {
let displayName: String
let action: () -> Void

var body: some View {
HStack(alignment: .center) {
Label(ElementL10n.roomTimelineReplyingTo(displayName), systemImage: "arrow.uturn.left")
.font(.element.caption2)
.foregroundColor(.element.secondaryContent)
.lineLimit(1)
Spacer()
Button {
action()
} label: {
Image(systemName: "xmark")
.font(.element.caption2)
.padding(4.0)
}
}
}
}

struct MessageComposer_Previews: PreviewProvider {
Expand All @@ -51,8 +100,20 @@ struct MessageComposer_Previews: PreviewProvider {
@ViewBuilder
static var body: some View {
VStack {
MessageComposer(text: .constant(""), disabled: true) { }
MessageComposer(text: .constant("Some message"), disabled: false) { }
MessageComposer(text: .constant(""),
focused: .constant(false),
sendingDisabled: true,
type: .default,
sendAction: { },
replyCancellationAction: { })

MessageComposer(text: .constant("Some message"),
focused: .constant(false),
sendingDisabled: false,
type: .reply(id: UUID().uuidString,
displayName: "John Doe"),
sendAction: { },
replyCancellationAction: { })
}
.tint(.element.accent)
}
Expand Down
Loading

0 comments on commit ffee192

Please sign in to comment.