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:
Stefan Ceriu
2022-12-21 11:18:45 +02:00
committed by GitHub
parent 5b9d45e461
commit 71f1f044c7
33 changed files with 170 additions and 534 deletions
@@ -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:
@@ -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
}
}
@@ -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
}
}
}
}
}
}
@@ -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