Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pinned events banner loading state #3118

Merged
merged 12 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 24 additions & 24 deletions ElementX.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/element-hq/matrix-rust-components-swift",
"state" : {
"revision" : "8e2b4049fb492dcf5b0c796784b7aa7a3c099943",
"version" : "1.0.31"
"revision" : "9a9d116e5f00b31ad4f727d0875fed13a230806b",
"version" : "1.0.32"
}
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@
"screen_room_mentions_at_room_subtitle" = "Notify the whole room";
"screen_room_pinned_banner_indicator" = "%1$@ of %2$@";
"screen_room_pinned_banner_indicator_description" = "%1$@ Pinned messages";
"screen_room_pinned_banner_loading_description" = "Loading message…";
"screen_room_pinned_banner_view_all_button_title" = "View All";
"screen_account_provider_change" = "Change account provider";
"screen_account_provider_form_hint" = "Homeserver address";
Expand Down Expand Up @@ -851,6 +852,12 @@
"state_event_room_name_removed_by_you" = "You removed the room name";
"state_event_room_none" = "%1$@ made no changes";
"state_event_room_none_by_you" = "You made no changes";
"state_event_room_pinned_events_changed" = "%1$@ changed the pinned messages";
"state_event_room_pinned_events_changed_by_you" = "You changed the pinned messages";
"state_event_room_pinned_events_pinned" = "%1$@ pinned a message";
"state_event_room_pinned_events_pinned_by_you" = "You pinned a message";
"state_event_room_pinned_events_unpinned" = "%1$@ unpinned a message";
"state_event_room_pinned_events_unpinned_by_you" = "You unpinned a message";
"state_event_room_reject" = "%1$@ rejected the invitation";
"state_event_room_reject_by_you" = "You rejected the invitation";
"state_event_room_remove" = "%1$@ removed %2$@";
Expand Down
20 changes: 20 additions & 0 deletions ElementX/Sources/Generated/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1675,6 +1675,8 @@ internal enum L10n {
internal static func screenRoomPinnedBannerIndicatorDescription(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_room_pinned_banner_indicator_description", String(describing: p1))
}
/// Loading message…
internal static var screenRoomPinnedBannerLoadingDescription: String { return L10n.tr("Localizable", "screen_room_pinned_banner_loading_description") }
/// View All
internal static var screenRoomPinnedBannerViewAllButtonTitle: String { return L10n.tr("Localizable", "screen_room_pinned_banner_view_all_button_title") }
/// Send again
Expand Down Expand Up @@ -2111,6 +2113,24 @@ internal enum L10n {
}
/// You made no changes
internal static var stateEventRoomNoneByYou: String { return L10n.tr("Localizable", "state_event_room_none_by_you") }
/// %1$@ changed the pinned messages
internal static func stateEventRoomPinnedEventsChanged(_ p1: Any) -> String {
return L10n.tr("Localizable", "state_event_room_pinned_events_changed", String(describing: p1))
}
/// You changed the pinned messages
internal static var stateEventRoomPinnedEventsChangedByYou: String { return L10n.tr("Localizable", "state_event_room_pinned_events_changed_by_you") }
/// %1$@ pinned a message
internal static func stateEventRoomPinnedEventsPinned(_ p1: Any) -> String {
return L10n.tr("Localizable", "state_event_room_pinned_events_pinned", String(describing: p1))
}
/// You pinned a message
internal static var stateEventRoomPinnedEventsPinnedByYou: String { return L10n.tr("Localizable", "state_event_room_pinned_events_pinned_by_you") }
/// %1$@ unpinned a message
internal static func stateEventRoomPinnedEventsUnpinned(_ p1: Any) -> String {
return L10n.tr("Localizable", "state_event_room_pinned_events_unpinned", String(describing: p1))
}
/// You unpinned a message
internal static var stateEventRoomPinnedEventsUnpinnedByYou: String { return L10n.tr("Localizable", "state_event_room_pinned_events_unpinned_by_you") }
/// %1$@ rejected the invitation
internal static func stateEventRoomReject(_ p1: Any) -> String {
return L10n.tr("Localizable", "state_event_room_reject", String(describing: p1))
Expand Down
26 changes: 26 additions & 0 deletions ElementX/Sources/Mocks/NetworkMonitorMock.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// Copyright 2024 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 Combine
import Foundation

extension NetworkMonitorMock {
static var `default`: NetworkMonitorMock {
let mock = NetworkMonitorMock()
mock.underlyingReachabilityPublisher = .init(.init(.reachable))
return mock
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
mediaPlayerProvider: parameters.mediaPlayerProvider,
voiceMessageMediaManager: parameters.voiceMessageMediaManager,
userIndicatorController: ServiceLocator.shared.userIndicatorController,
networkMonitor: ServiceLocator.shared.networkMonitor,
appMediator: parameters.appMediator,
appSettings: parameters.appSettings,
analyticsService: ServiceLocator.shared.analytics)
Expand Down
107 changes: 95 additions & 12 deletions ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,10 @@ struct RoomScreenViewState: BindableState {
// It's updated from the room info, so it's faster than using the timeline
var pinnedEventIDs: Set<String> = []
// This is used to control the banner
var pinnedEventsState = PinnedEventsState()
var pinnedEventsBannerState: PinnedEventsBannerState = .loading(numbersOfEvents: 0)

var shouldShowPinnedEventsBanner: Bool {
isPinningEnabled && !pinnedEventsState.pinnedEventContents.isEmpty && lastScrollDirection != .top
isPinningEnabled && !pinnedEventsBannerState.isEmpty && lastScrollDirection != .top
}

var canJoinCall = false
Expand Down Expand Up @@ -306,11 +306,12 @@ struct PinnedEventsState: Equatable {
var pinnedEventContents: OrderedDictionary<String, AttributedString> = [:] {
didSet {
if selectedPinEventID == nil, !pinnedEventContents.keys.isEmpty {
// The default selected event should always be the last one.
selectedPinEventID = pinnedEventContents.keys.last
} else if pinnedEventContents.isEmpty {
selectedPinEventID = nil
} else if let selectedPinEventID, !pinnedEventContents.keys.set.contains(selectedPinEventID) {
self.selectedPinEventID = pinnedEventContents.firstNonNil { $0.key }
self.selectedPinEventID = pinnedEventContents.keys.last
}
}
}
Expand All @@ -326,30 +327,112 @@ struct PinnedEventsState: Equatable {
}

var selectedPinContent: AttributedString {
guard let selectedPinEventID,
var content = pinnedEventContents[selectedPinEventID] else {
return AttributedString()
var content = AttributedString(" ")
if let selectedPinEventID,
var pinnedEventContent = pinnedEventContents[selectedPinEventID] {
content = pinnedEventContent
}
content.font = .compound.bodyMD
return content
}

mutating func nextPin() {
guard !pinnedEventContents.isEmpty else {
return
}
let currentIndex = selectedPinIndex
let nextIndex = (currentIndex + 1) % pinnedEventContents.count
selectedPinEventID = pinnedEventContents.keys[nextIndex]
}
}

enum PinnedEventsBannerState: Equatable {
case loading(numbersOfEvents: Int)
case loaded(state: PinnedEventsState)

var isEmpty: Bool {
switch self {
case .loaded(let state):
return state.pinnedEventContents.isEmpty
case .loading(let numberOfEvents):
return numberOfEvents == 0
}
}

var isLoading: Bool {
switch self {
case .loading:
return true
default:
return false
}
}

var selectedPinEventID: String? {
switch self {
case .loaded(let state):
return state.selectedPinEventID
default:
return nil
}
}

var count: Int {
switch self {
case .loaded(let state):
return state.pinnedEventContents.count
case .loading(let numberOfEvents):
return numberOfEvents
}
}

var selectedPinIndex: Int {
switch self {
case .loaded(let state):
return state.selectedPinIndex
case .loading(let numbersOfEvents):
// We always want the index to be the last one when loading, since is the default one.
return numbersOfEvents - 1
Velin92 marked this conversation as resolved.
Show resolved Hide resolved
}
}

var displayedMessage: AttributedString {
switch self {
case .loading:
return AttributedString(L10n.screenRoomPinnedBannerLoadingDescription)
case .loaded(let state):
return state.selectedPinContent
}
}

var bannerIndicatorDescription: AttributedString {
let index = selectedPinIndex + 1
let boldPlaceholder = "{bold}"
var finalString = AttributedString(L10n.screenRoomPinnedBannerIndicatorDescription(boldPlaceholder))
var boldString = AttributedString(L10n.screenRoomPinnedBannerIndicator(index, pinnedEventContents.count))
var boldString = AttributedString(L10n.screenRoomPinnedBannerIndicator(index, count))
boldString.bold()
finalString.replace(boldPlaceholder, with: boldString)
return finalString
}

mutating func nextPin() {
guard !pinnedEventContents.isEmpty else {
return
switch self {
case .loaded(var state):
state.nextPin()
self = .loaded(state: state)
default:
break
}
}

mutating func setPinnedEventContents(_ pinnedEventContents: OrderedDictionary<String, AttributedString>) {
switch self {
case .loading:
// The default selected event should always be the last one.
self = .loaded(state: .init(pinnedEventContents: pinnedEventContents, selectedPinEventID: pinnedEventContents.keys.last))
case .loaded(var state):
state.pinnedEventContents = pinnedEventContents
self = .loaded(state: state)
}
let currentIndex = selectedPinIndex
let nextIndex = (currentIndex + 1) % pinnedEventContents.count
selectedPinEventID = pinnedEventContents.keys[nextIndex]
}
}
Loading
Loading