mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 21:39:12 +00:00
Fixes vector-im/element-x-ios/issues/114 - Replying to timeline items
This commit is contained in:
parent
dd2385711a
commit
ffee192e89
@ -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!";
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
@ -33,6 +39,7 @@ enum RoomScreenViewAction {
|
||||
case linkClicked(url: URL)
|
||||
case sendMessage
|
||||
case sendReaction(key: String, eventID: String)
|
||||
case cancelReply
|
||||
}
|
||||
|
||||
struct RoomScreenViewState: BindableState {
|
||||
@ -45,6 +52,9 @@ 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
|
||||
}
|
||||
@ -52,6 +62,7 @@ struct RoomScreenViewState: BindableState {
|
||||
|
||||
struct RoomScreenViewStateBindings {
|
||||
var composerText: String
|
||||
var composerFocused: Bool
|
||||
|
||||
/// Information describing the currently displayed alert.
|
||||
var alertInfo: AlertInfo<RoomScreenErrorType>?
|
||||
|
@ -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)
|
||||
@ -75,7 +75,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
default:
|
||||
state.isBackPaginating = false
|
||||
}
|
||||
|
||||
case .itemAppeared(let id):
|
||||
await timelineController.processItemAppearance(id)
|
||||
case .itemDisappeared(let id):
|
||||
@ -83,15 +82,12 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
@ -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.")
|
||||
@ -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 {
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
@ -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)
|
||||
}
|
||||
|
@ -18,8 +18,9 @@ import SwiftUI
|
||||
|
||||
struct MessageComposerTextField: View {
|
||||
@Binding private var text: String
|
||||
@Binding private var focused: Bool
|
||||
|
||||
@State private var dynamicHeight: CGFloat = 100
|
||||
@State private var isEditing = false
|
||||
|
||||
private let placeholder: String
|
||||
private let maxHeight: CGFloat
|
||||
@ -28,36 +29,24 @@ struct MessageComposerTextField: View {
|
||||
text.isEmpty
|
||||
}
|
||||
|
||||
init(placeholder: String, text: Binding<String>, maxHeight: CGFloat) {
|
||||
init(placeholder: String, text: Binding<String>, focused: Binding<Bool>, maxHeight: CGFloat) {
|
||||
self.placeholder = placeholder
|
||||
_text = text
|
||||
_focused = focused
|
||||
self.maxHeight = maxHeight
|
||||
}
|
||||
|
||||
private var placeholderColor: Color {
|
||||
.gray
|
||||
}
|
||||
|
||||
private var borderColor: Color {
|
||||
.element.accent
|
||||
}
|
||||
|
||||
private var borderWidth: CGFloat {
|
||||
isEditing ? 2.0 : 1.0
|
||||
.element.secondaryContent
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let rect = RoundedRectangle(cornerRadius: 8.0)
|
||||
return UITextViewWrapper(text: $text,
|
||||
calculatedHeight: $dynamicHeight,
|
||||
isEditing: $isEditing,
|
||||
maxHeight: maxHeight)
|
||||
UITextViewWrapper(text: $text,
|
||||
calculatedHeight: $dynamicHeight,
|
||||
focused: $focused,
|
||||
maxHeight: maxHeight)
|
||||
.frame(minHeight: dynamicHeight, maxHeight: dynamicHeight)
|
||||
.padding(4.0)
|
||||
.background(placeholderView, alignment: .topLeading)
|
||||
.clipShape(rect)
|
||||
.overlay(rect.stroke(borderColor, lineWidth: borderWidth))
|
||||
.animation(.elementDefault, value: isEditing)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@ -65,8 +54,6 @@ struct MessageComposerTextField: View {
|
||||
if showingPlaceholder {
|
||||
Text(placeholder)
|
||||
.foregroundColor(placeholderColor)
|
||||
.padding(.leading, 8.0)
|
||||
.padding(.top, 12.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -76,7 +63,7 @@ private struct UITextViewWrapper: UIViewRepresentable {
|
||||
|
||||
@Binding var text: String
|
||||
@Binding var calculatedHeight: CGFloat
|
||||
@Binding var isEditing: Bool
|
||||
@Binding var focused: Bool
|
||||
|
||||
let maxHeight: CGFloat
|
||||
|
||||
@ -90,23 +77,33 @@ private struct UITextViewWrapper: UIViewRepresentable {
|
||||
textView.isUserInteractionEnabled = true
|
||||
textView.backgroundColor = UIColor.clear
|
||||
textView.returnKeyType = .default
|
||||
textView.textContainer.lineFragmentPadding = 0.0
|
||||
textView.textContainerInset = .zero
|
||||
|
||||
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||
|
||||
return textView
|
||||
}
|
||||
|
||||
func updateUIView(_ view: UITextView, context: UIViewRepresentableContext<UITextViewWrapper>) {
|
||||
if view.text != text {
|
||||
view.text = text
|
||||
func updateUIView(_ textView: UITextView, context: UIViewRepresentableContext<UITextViewWrapper>) {
|
||||
if textView.text != text {
|
||||
textView.text = text
|
||||
}
|
||||
|
||||
UITextViewWrapper.recalculateHeight(view: view, result: $calculatedHeight, maxHeight: maxHeight)
|
||||
UITextViewWrapper.recalculateHeight(view: textView, result: $calculatedHeight, maxHeight: maxHeight)
|
||||
|
||||
if focused, textView.window != nil, !textView.isFirstResponder {
|
||||
// Avoid cycle detected through attribute warnings
|
||||
DispatchQueue.main.async {
|
||||
textView.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(text: $text,
|
||||
height: $calculatedHeight,
|
||||
isEditing: $isEditing,
|
||||
focused: $focused,
|
||||
maxHeight: maxHeight)
|
||||
}
|
||||
|
||||
@ -117,22 +114,22 @@ private struct UITextViewWrapper: UIViewRepresentable {
|
||||
|
||||
if result.wrappedValue != height {
|
||||
DispatchQueue.main.async {
|
||||
result.wrappedValue = height // !! must be called asynchronously
|
||||
result.wrappedValue = height // Must be called asynchronously
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class Coordinator: NSObject, UITextViewDelegate {
|
||||
var text: Binding<String>
|
||||
var calculatedHeight: Binding<CGFloat>
|
||||
var isEditing: Binding<Bool>
|
||||
private var text: Binding<String>
|
||||
private var calculatedHeight: Binding<CGFloat>
|
||||
private var focused: Binding<Bool>
|
||||
|
||||
let maxHeight: CGFloat
|
||||
private let maxHeight: CGFloat
|
||||
|
||||
init(text: Binding<String>, height: Binding<CGFloat>, isEditing: Binding<Bool>, maxHeight: CGFloat) {
|
||||
init(text: Binding<String>, height: Binding<CGFloat>, focused: Binding<Bool>, maxHeight: CGFloat) {
|
||||
self.text = text
|
||||
calculatedHeight = height
|
||||
self.isEditing = isEditing
|
||||
self.focused = focused
|
||||
self.maxHeight = maxHeight
|
||||
}
|
||||
|
||||
@ -143,16 +140,12 @@ private struct UITextViewWrapper: UIViewRepresentable {
|
||||
maxHeight: maxHeight)
|
||||
}
|
||||
|
||||
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||
true
|
||||
}
|
||||
|
||||
func textViewDidBeginEditing(_ textView: UITextView) {
|
||||
isEditing.wrappedValue = true
|
||||
focused.wrappedValue = true
|
||||
}
|
||||
|
||||
func textViewDidEndEditing(_ textView: UITextView) {
|
||||
isEditing.wrappedValue = false
|
||||
focused.wrappedValue = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -173,13 +166,15 @@ struct MessageComposerTextField_Previews: PreviewProvider {
|
||||
|
||||
struct PreviewWrapper: View {
|
||||
@State var text: String
|
||||
@State var focused: Bool
|
||||
|
||||
init(text: String) {
|
||||
_text = .init(initialValue: text)
|
||||
_focused = .init(initialValue: false)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
MessageComposerTextField(placeholder: "Placeholder", text: $text, maxHeight: 300)
|
||||
MessageComposerTextField(placeholder: "Placeholder", text: $text, focused: $focused, maxHeight: 300)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,10 +24,16 @@ struct RoomScreen: View {
|
||||
TimelineView()
|
||||
.environmentObject(context)
|
||||
|
||||
MessageComposer(text: $context.composerText, disabled: context.viewState.sendButtonDisabled) {
|
||||
MessageComposer(text: $context.composerText,
|
||||
focused: $context.composerFocused,
|
||||
sendingDisabled: context.viewState.sendButtonDisabled,
|
||||
type: context.viewState.composerMode) {
|
||||
sendMessage()
|
||||
} replyCancellationAction: {
|
||||
context.send(viewAction: .cancelReply)
|
||||
}
|
||||
.padding()
|
||||
.opacity(context.viewState.messageComposerDisabled ? 0.5 : 1.0)
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
|
@ -36,6 +36,10 @@ public struct TimelineItemContextMenu: View {
|
||||
Button { callback(item) } label: {
|
||||
Label(ElementL10n.permalink, systemImage: "link")
|
||||
}
|
||||
case .reply:
|
||||
Button { callback(item) } label: {
|
||||
Label(ElementL10n.reply, systemImage: "arrow.uturn.left")
|
||||
}
|
||||
case .redact:
|
||||
Button(role: .destructive) { callback(item) } label: {
|
||||
Label(ElementL10n.messageActionItemRedact, systemImage: "trash")
|
||||
|
@ -48,6 +48,7 @@ struct TimelineItemList: View {
|
||||
.contextMenu(menuItems: {
|
||||
context.viewState.contextMenuBuilder?(timelineItem.id)
|
||||
})
|
||||
.opacity(opacityForItem(timelineItem))
|
||||
.listRowBackground(Color.clear)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowInsets(settings.timelineStyle.listRowInsets)
|
||||
@ -142,6 +143,14 @@ struct TimelineItemList: View {
|
||||
context.send(viewAction: .loadPreviousPage)
|
||||
}
|
||||
|
||||
private func opacityForItem(_ item: RoomTimelineViewProvider) -> Double {
|
||||
guard case let .reply(selectedItemId, _) = context.viewState.composerMode else {
|
||||
return 1.0
|
||||
}
|
||||
|
||||
return selectedItemId == item.id ? 1.0 : 0.5
|
||||
}
|
||||
|
||||
private var isPreview: Bool {
|
||||
#if DEBUG
|
||||
return ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"
|
||||
|
@ -54,7 +54,7 @@ struct MockRoomProxy: RoomProxyProtocol {
|
||||
.failure(.backwardStreamNotAvailable)
|
||||
}
|
||||
|
||||
func sendMessage(_ message: String) async -> Result<Void, RoomProxyError> {
|
||||
func sendMessage(_ message: String, inReplyToEventId: String? = nil) async -> Result<Void, RoomProxyError> {
|
||||
.failure(.failedSendingMessage)
|
||||
}
|
||||
|
||||
|
@ -175,17 +175,24 @@ class RoomProxy: RoomProxyProtocol {
|
||||
.value
|
||||
}
|
||||
|
||||
func sendMessage(_ message: String) async -> Result<Void, RoomProxyError> {
|
||||
func sendMessage(_ message: String, inReplyToEventId: String? = nil) async -> Result<Void, RoomProxyError> {
|
||||
sendMessageBgTask = backgroundTaskService.startBackgroundTask(withName: "SendMessage", isReusable: true)
|
||||
defer {
|
||||
sendMessageBgTask?.stop()
|
||||
}
|
||||
let messageContent = messageEventContentFromMarkdown(md: message)
|
||||
|
||||
let transactionId = genTransactionId()
|
||||
|
||||
return await Task(priority: .high) { () -> Result<Void, RoomProxyError> in
|
||||
do {
|
||||
// Disabled until available in Rust
|
||||
// if let inReplyToEventId = inReplyToEventId {
|
||||
// #warning("Markdown support when available in Ruma")
|
||||
// try self.room.sendReply(msg: message, inReplyToEventId: inReplyToEventId, txnId: transactionId)
|
||||
// } else {
|
||||
let messageContent = messageEventContentFromMarkdown(md: message)
|
||||
try self.room.send(msg: messageContent, txnId: transactionId)
|
||||
// }
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(.failedSendingMessage)
|
||||
|
@ -55,9 +55,15 @@ protocol RoomProxyProtocol {
|
||||
|
||||
func paginateBackwards(count: UInt) async -> Result<Void, RoomProxyError>
|
||||
|
||||
func sendMessage(_ message: String) async -> Result<Void, RoomProxyError>
|
||||
func sendMessage(_ message: String, inReplyToEventId: String?) async -> Result<Void, RoomProxyError>
|
||||
|
||||
func redact(_ eventID: String) async -> Result<Void, RoomProxyError>
|
||||
|
||||
var callbacks: PassthroughSubject<RoomProxyCallback, Never> { get }
|
||||
}
|
||||
|
||||
extension RoomProxyProtocol {
|
||||
func sendMessage(_ message: String) async -> Result<Void, RoomProxyError> {
|
||||
await sendMessage(message, inReplyToEventId: nil)
|
||||
}
|
||||
}
|
||||
|
@ -66,5 +66,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
|
||||
|
||||
func sendMessage(_ message: String) async { }
|
||||
|
||||
func sendReply(_ message: String, to itemId: String) async { }
|
||||
|
||||
func redact(_ eventID: String) async { }
|
||||
}
|
||||
|
@ -104,6 +104,13 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func sendReply(_ message: String, to itemId: String) async {
|
||||
switch await timelineProvider.sendMessage(message, inReplyToItemId: itemId) {
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func redact(_ eventID: String) async {
|
||||
switch await timelineProvider.redact(eventID) {
|
||||
default:
|
||||
|
@ -41,5 +41,7 @@ protocol RoomTimelineControllerProtocol {
|
||||
|
||||
func sendMessage(_ message: String) async
|
||||
|
||||
func sendReply(_ message: String, to itemId: String) async
|
||||
|
||||
func redact(_ eventID: String) async
|
||||
}
|
||||
|
@ -51,8 +51,8 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
func sendMessage(_ message: String) async -> Result<Void, RoomTimelineProviderError> {
|
||||
switch await roomProxy.sendMessage(message) {
|
||||
func sendMessage(_ message: String, inReplyToItemId: String?) async -> Result<Void, RoomTimelineProviderError> {
|
||||
switch await roomProxy.sendMessage(message, inReplyToEventId: inReplyToItemId) {
|
||||
case .success:
|
||||
return .success(())
|
||||
case .failure:
|
||||
|
@ -35,7 +35,13 @@ protocol RoomTimelineProviderProtocol {
|
||||
|
||||
func paginateBackwards(_ count: UInt) async -> Result<Void, RoomTimelineProviderError>
|
||||
|
||||
func sendMessage(_ message: String) async -> Result<Void, RoomTimelineProviderError>
|
||||
func sendMessage(_ message: String, inReplyToItemId: String?) async -> Result<Void, RoomTimelineProviderError>
|
||||
|
||||
func redact(_ eventID: String) async -> Result<Void, RoomTimelineProviderError>
|
||||
}
|
||||
|
||||
extension RoomTimelineProviderProtocol {
|
||||
func sendMessage(_ message: String) async -> Result<Void, RoomTimelineProviderError> {
|
||||
await sendMessage(message, inReplyToItemId: nil)
|
||||
}
|
||||
}
|
||||
|
1
changelog.d/114.feature
Normal file
1
changelog.d/114.feature
Normal file
@ -0,0 +1 @@
|
||||
Implemented timeline item repyling
|
Loading…
x
Reference in New Issue
Block a user