mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Various tweaks (#381)
* Add the message delivery status to the plain timeline styler and display it outside of the main content * Fix glitchy timeline scroll to bottom button * Fixed bubbled reactions padding * Simplified the emoji picker, double tapping a timeline item directly opens it now and added a context menu option. Linked it to rust side reaction sending * Fix the sliding sync cold cache: treat invalidated rooms as filled * Make splash screen view full screen * Fix the offline indicator popping up when first setting up the network monitor * Expose presentationDetents on the NavigationStackCoordinator and start using them for the EmojiPicker * Fix link tint color * Linked TimelineReactionsView reaction sending * Remove now unused/unnecessary classes * Add changelog * Fix formatting issue
This commit is contained in:
@@ -107,6 +107,9 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
let timelineController = parameters?.timelineController else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
let emojiPickerNavigationStackCoordinator = NavigationStackCoordinator()
|
||||
|
||||
let params = EmojiPickerScreenCoordinatorParameters(emojiProvider: emojiProvider,
|
||||
itemId: itemId)
|
||||
let coordinator = EmojiPickerScreenCoordinator(parameters: params)
|
||||
@@ -118,10 +121,15 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
||||
Task {
|
||||
await timelineController.sendReaction(emoji, for: itemId)
|
||||
}
|
||||
case .dismiss:
|
||||
self?.navigationStackCoordinator.setSheetCoordinator(nil)
|
||||
}
|
||||
}
|
||||
|
||||
navigationStackCoordinator.setSheetCoordinator(coordinator)
|
||||
emojiPickerNavigationStackCoordinator.setRootCoordinator(coordinator)
|
||||
emojiPickerNavigationStackCoordinator.presentationDetents = [.medium, .large]
|
||||
|
||||
navigationStackCoordinator.setSheetCoordinator(emojiPickerNavigationStackCoordinator)
|
||||
}
|
||||
|
||||
private func displayRoomDetails() {
|
||||
|
||||
@@ -33,7 +33,6 @@ enum RoomScreenComposerMode: Equatable {
|
||||
enum RoomScreenViewAction {
|
||||
case headerTapped
|
||||
case displayEmojiPicker(itemId: String)
|
||||
case emojiTapped(emoji: String, itemId: String)
|
||||
case paginateBackwards
|
||||
case itemAppeared(id: String)
|
||||
case itemDisappeared(id: String)
|
||||
@@ -43,7 +42,6 @@ enum RoomScreenViewAction {
|
||||
case sendReaction(key: String, eventID: String)
|
||||
case cancelReply
|
||||
case cancelEdit
|
||||
case displayReactionsMenuForItemId(itemId: String)
|
||||
}
|
||||
|
||||
struct RoomScreenViewState: BindableState {
|
||||
@@ -54,7 +52,6 @@ struct RoomScreenViewState: BindableState {
|
||||
var isBackPaginating = false
|
||||
var showLoading = false
|
||||
var bindings: RoomScreenViewStateBindings
|
||||
var displayReactionsMenuForItemId = ""
|
||||
|
||||
var contextMenuBuilder: (@MainActor (_ itemId: String) -> TimelineItemContextMenu)?
|
||||
|
||||
|
||||
@@ -102,20 +102,14 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
MXLog.warning("Link clicked: \(url)")
|
||||
case .sendMessage:
|
||||
await sendCurrentMessage()
|
||||
case .sendReaction(let key, _):
|
||||
#warning("Reaction implementation awaiting SDK support.")
|
||||
MXLog.warning("React with \(key) failed. Not implemented.")
|
||||
case .sendReaction(let emoji, let itemId):
|
||||
await timelineController.sendReaction(emoji, for: itemId)
|
||||
case .displayEmojiPicker(let itemId):
|
||||
callback?(.displayEmojiPicker(itemId: itemId))
|
||||
case .displayReactionsMenuForItemId(let itemId):
|
||||
state.displayReactionsMenuForItemId = itemId
|
||||
case .cancelReply:
|
||||
state.composerMode = .default
|
||||
case .cancelEdit:
|
||||
state.composerMode = .default
|
||||
case .emojiTapped(let emoji, let itemId):
|
||||
await timelineController.sendReaction(emoji, for: itemId)
|
||||
state.displayReactionsMenuForItemId = ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,8 +122,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
|
||||
private func paginateBackwards() async {
|
||||
switch await timelineController.paginateBackwards(Constants.backPaginationPageSize) {
|
||||
case .failure:
|
||||
displayError(.alert(ElementL10n.roomTimelineBackpaginationFailure))
|
||||
default:
|
||||
#warning("Treat errors")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +197,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
}
|
||||
|
||||
var actions: [TimelineItemContextMenuAction] = [
|
||||
.copy, .quote, .copyPermalink, .reply
|
||||
.react, .copy, .quote, .copyPermalink, .reply
|
||||
]
|
||||
|
||||
if item.isEditable {
|
||||
@@ -215,6 +211,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
return .init(actions: actions)
|
||||
}
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
private func processContentMenuAction(_ action: TimelineItemContextMenuAction, itemId: String) {
|
||||
guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemId }),
|
||||
let item = timelineItem as? EventBasedTimelineItemProtocol else {
|
||||
@@ -222,6 +219,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
||||
}
|
||||
|
||||
switch action {
|
||||
case .react:
|
||||
callback?(.displayEmojiPicker(itemId: item.id))
|
||||
case .copy:
|
||||
UIPasteboard.general.string = item.text
|
||||
case .edit:
|
||||
|
||||
+18
-22
@@ -27,28 +27,25 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
@ScaledMetric private var senderNameVerticalPadding = 3
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: alignment, spacing: -12) {
|
||||
if !timelineItem.isOutgoing {
|
||||
header
|
||||
.zIndex(1)
|
||||
}
|
||||
VStack(alignment: alignment) {
|
||||
if timelineItem.isOutgoing {
|
||||
HStack {
|
||||
Spacer()
|
||||
styledContentWithReactions
|
||||
if timelineItem.isOutgoing {
|
||||
TimelineDeliveryStatusView(deliveryStatus: timelineItem.properties.deliveryStatus)
|
||||
.padding(.top, 6)
|
||||
}
|
||||
}
|
||||
.padding(.trailing, 16)
|
||||
.padding(.leading, 56)
|
||||
} else {
|
||||
styledContentWithReactions
|
||||
.padding(.leading, 24)
|
||||
.padding(.trailing, 56)
|
||||
ZStack(alignment: .trailingFirstTextBaseline) {
|
||||
VStack(alignment: alignment, spacing: -12) {
|
||||
if !timelineItem.isOutgoing {
|
||||
header
|
||||
.zIndex(1)
|
||||
}
|
||||
|
||||
HStack {
|
||||
if timelineItem.isOutgoing {
|
||||
Spacer()
|
||||
}
|
||||
|
||||
styledContentWithReactions
|
||||
}
|
||||
.padding(.horizontal, 16.0)
|
||||
}
|
||||
|
||||
if timelineItem.isOutgoing {
|
||||
TimelineDeliveryStatusView(deliveryStatus: timelineItem.properties.deliveryStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,7 +82,6 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
alignment: alignment) { key in
|
||||
context.send(viewAction: .sendReaction(key: key, eventID: timelineItem.id))
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,15 @@ struct TimelineItemPlainStylerView<Content: View>: View {
|
||||
header
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
content()
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
content()
|
||||
|
||||
Spacer()
|
||||
|
||||
if timelineItem.isOutgoing {
|
||||
TimelineDeliveryStatusView(deliveryStatus: timelineItem.properties.deliveryStatus)
|
||||
}
|
||||
}
|
||||
supplementaryViews
|
||||
}
|
||||
}
|
||||
|
||||
+10
-9
@@ -43,17 +43,18 @@ struct TimelineDeliveryStatusView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if showDeliveryStatus {
|
||||
Image(systemName: systemImageName)
|
||||
.task {
|
||||
if case .sent = deliveryStatus {
|
||||
try? await Task.sleep(nanoseconds: 1_000_000_000)
|
||||
withAnimation {
|
||||
showDeliveryStatus = false
|
||||
}
|
||||
Image(systemName: systemImageName)
|
||||
.resizable()
|
||||
.frame(width: 12.0, height: 12.0)
|
||||
.opacity(showDeliveryStatus ? 1.0 : 0.0)
|
||||
.task {
|
||||
if case .sent = deliveryStatus {
|
||||
try? await Task.sleep(nanoseconds: 1_000_000_000)
|
||||
withAnimation {
|
||||
showDeliveryStatus = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-56
@@ -1,56 +0,0 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct TimelineItemReactionsMenuView: View {
|
||||
private let emojis = ["👍🏼", "👎🏼", "😄", "🙏🏼", "😇"]
|
||||
|
||||
var onEmojiSelected: ((String) -> Void)?
|
||||
var onDisplayEmojiPicker: (() -> Void)?
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
HStack(spacing: 10) {
|
||||
ForEach(emojis, id: \.self) { emoji in
|
||||
Button {
|
||||
onEmojiSelected?(emoji)
|
||||
} label: {
|
||||
Text(emoji)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(10)
|
||||
.background(.gray)
|
||||
.cornerRadius(15)
|
||||
HStack(spacing: 10) {
|
||||
Text("➕")
|
||||
}
|
||||
.padding(10)
|
||||
.background(.gray)
|
||||
.cornerRadius(15)
|
||||
.onTapGesture {
|
||||
onDisplayEmojiPicker?()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TimelineItemReactionsMenuView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TimelineItemReactionsMenuView(onDisplayEmojiPicker: nil)
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,8 @@ struct FormattedBodyText: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.tint(.element.accent)
|
||||
// Should be .element.accent but that's currently black
|
||||
.tint(.blue)
|
||||
}
|
||||
|
||||
private var blockquoteAttributes: AttributeContainer {
|
||||
|
||||
@@ -22,6 +22,7 @@ struct TimelineItemContextMenuActions {
|
||||
}
|
||||
|
||||
enum TimelineItemContextMenuAction: Identifiable, Hashable {
|
||||
case react
|
||||
case copy
|
||||
case edit
|
||||
case quote
|
||||
@@ -59,6 +60,10 @@ public struct TimelineItemContextMenu: View {
|
||||
private func viewsForActions(_ actions: [TimelineItemContextMenuAction]) -> some View {
|
||||
ForEach(actions, id: \.self) { item in
|
||||
switch item {
|
||||
case .react:
|
||||
Button { callback(item) } label: {
|
||||
Label(ElementL10n.reactions, systemImage: "face.smiling")
|
||||
}
|
||||
case .copy:
|
||||
Button { callback(item) } label: {
|
||||
Label(ElementL10n.actionCopy, systemImage: "doc.on.doc")
|
||||
|
||||
@@ -69,13 +69,7 @@ class TimelineTableViewController: UIViewController {
|
||||
applySnapshot()
|
||||
}
|
||||
}
|
||||
|
||||
var displayReactionsMenuForItemId = "" {
|
||||
didSet {
|
||||
tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var contextMenuBuilder: (@MainActor (_ itemId: String) -> TimelineItemContextMenu)?
|
||||
|
||||
@Binding private var scrollToBottomButtonVisible: Bool
|
||||
@@ -193,7 +187,6 @@ class TimelineTableViewController: UIViewController {
|
||||
// A local reference to avoid capturing self in the cell configuration.
|
||||
let coordinator = self.coordinator
|
||||
let opacity = self.opacity(for: timelineItem)
|
||||
let displayReactionsMenuForItemId = self.displayReactionsMenuForItemId
|
||||
let contextMenuBuilder = self.contextMenuBuilder
|
||||
|
||||
cell.item = timelineItem
|
||||
@@ -201,38 +194,28 @@ class TimelineTableViewController: UIViewController {
|
||||
if case .backPaginationIndicator = timelineItem {
|
||||
timelineItem
|
||||
} else {
|
||||
VStack {
|
||||
if displayReactionsMenuForItemId == timelineItem.id {
|
||||
TimelineItemReactionsMenuView { emoji in
|
||||
coordinator.send(viewAction: .emojiTapped(emoji: emoji, itemId: timelineItem.id))
|
||||
} onDisplayEmojiPicker: {
|
||||
coordinator.send(viewAction: .displayEmojiPicker(itemId: timelineItem.id))
|
||||
}
|
||||
timelineItem
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.opacity(opacity)
|
||||
.contextMenu {
|
||||
contextMenuBuilder?(timelineItem.id)
|
||||
}
|
||||
.onAppear {
|
||||
coordinator.send(viewAction: .itemAppeared(id: timelineItem.id))
|
||||
}
|
||||
.onDisappear {
|
||||
coordinator.send(viewAction: .itemDisappeared(id: timelineItem.id))
|
||||
}
|
||||
.environment(\.openURL, OpenURLAction { url in
|
||||
coordinator.send(viewAction: .linkClicked(url: url))
|
||||
return .systemAction
|
||||
})
|
||||
.onTapGesture(count: 2) {
|
||||
coordinator.send(viewAction: .displayEmojiPicker(itemId: timelineItem.id))
|
||||
}
|
||||
.onTapGesture {
|
||||
coordinator.send(viewAction: .itemTapped(id: timelineItem.id))
|
||||
}
|
||||
|
||||
timelineItem
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.opacity(opacity)
|
||||
.contextMenu {
|
||||
contextMenuBuilder?(timelineItem.id)
|
||||
}
|
||||
.onAppear {
|
||||
coordinator.send(viewAction: .itemAppeared(id: timelineItem.id))
|
||||
}
|
||||
.onDisappear {
|
||||
coordinator.send(viewAction: .itemDisappeared(id: timelineItem.id))
|
||||
}
|
||||
.environment(\.openURL, OpenURLAction { url in
|
||||
coordinator.send(viewAction: .linkClicked(url: url))
|
||||
return .systemAction
|
||||
})
|
||||
.onTapGesture(count: 2) {
|
||||
coordinator.send(viewAction: .displayReactionsMenuForItemId(itemId: timelineItem.id))
|
||||
}
|
||||
.onTapGesture {
|
||||
coordinator.send(viewAction: .itemTapped(id: timelineItem.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.margins(.all, self.timelineStyle.rowInsets)
|
||||
@@ -375,15 +358,9 @@ class TimelineTableViewController: UIViewController {
|
||||
|
||||
extension TimelineTableViewController: UITableViewDelegate {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
let isAtBottom = isAtBottom()
|
||||
|
||||
// Dispatches fix runtime warnings about making changes during a view update.
|
||||
if !scrollToBottomButtonVisible, isAtBottom {
|
||||
DispatchQueue.main.async { self.scrollToBottomButtonVisible = true }
|
||||
} else if scrollToBottomButtonVisible, !isAtBottom {
|
||||
DispatchQueue.main.async { self.scrollToBottomButtonVisible = false }
|
||||
}
|
||||
|
||||
DispatchQueue.main.async { self.scrollToBottomButtonVisible = self.isAtBottom() }
|
||||
|
||||
paginateBackwardsPublisher.send(())
|
||||
}
|
||||
|
||||
|
||||
@@ -65,9 +65,6 @@ struct TimelineView: UIViewControllerRepresentable {
|
||||
if tableViewController.composerMode != context.viewState.composerMode {
|
||||
tableViewController.composerMode = context.viewState.composerMode
|
||||
}
|
||||
if tableViewController.displayReactionsMenuForItemId != context.viewState.displayReactionsMenuForItemId {
|
||||
tableViewController.displayReactionsMenuForItemId = context.viewState.displayReactionsMenuForItemId
|
||||
}
|
||||
|
||||
// Doesn't have an equatable conformance :(
|
||||
tableViewController.contextMenuBuilder = context.viewState.contextMenuBuilder
|
||||
|
||||
Reference in New Issue
Block a user