diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index c057e43d7..2a29ae11c 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2153,6 +2153,10 @@ Tap the + to start adding people."; "location_sharing_live_timer_incoming" = "Live until %@"; "location_sharing_live_loading" = "Loading Live location..."; "location_sharing_live_error" = "Live location error"; +"location_sharing_live_timer_selector_title" = "Choose for how long others will see your accurate location."; +"location_sharing_live_timer_selector_short" = "for 15 minutes"; +"location_sharing_live_timer_selector_medium" = "for 1 hour"; +"location_sharing_live_timer_selector_long" = "for 8 hours"; "location_sharing_live_no_user_locations_error_title" = "No user locations available"; "location_sharing_live_stop_sharing_error" = "Fail to stop sharing location"; "location_sharing_live_stop_sharing_progress" = "Stop location sharing"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 4ed449992..bc0111afb 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2819,6 +2819,22 @@ public class VectorL10n: NSObject { public static func locationSharingLiveTimerIncoming(_ p1: String) -> String { return VectorL10n.tr("Vector", "location_sharing_live_timer_incoming", p1) } + /// for 8 hours + public static var locationSharingLiveTimerSelectorLong: String { + return VectorL10n.tr("Vector", "location_sharing_live_timer_selector_long") + } + /// for 1 hour + public static var locationSharingLiveTimerSelectorMedium: String { + return VectorL10n.tr("Vector", "location_sharing_live_timer_selector_medium") + } + /// for 15 minutes + public static var locationSharingLiveTimerSelectorShort: String { + return VectorL10n.tr("Vector", "location_sharing_live_timer_selector_short") + } + /// Choose for how long others will see your accurate location. + public static var locationSharingLiveTimerSelectorTitle: String { + return VectorL10n.tr("Vector", "location_sharing_live_timer_selector_title") + } /// Location public static var locationSharingLiveViewerTitle: String { return VectorL10n.tr("Vector", "location_sharing_live_viewer_title") diff --git a/Riot/Modules/Application/ScreenNavigation/RoomNavigationParameters.swift b/Riot/Modules/Application/ScreenNavigation/RoomNavigationParameters.swift index 3f799d82b..9a25f2dce 100644 --- a/Riot/Modules/Application/ScreenNavigation/RoomNavigationParameters.swift +++ b/Riot/Modules/Application/ScreenNavigation/RoomNavigationParameters.swift @@ -85,13 +85,22 @@ class RoomNavigationParameters: NSObject { super.init() } - convenience init(roomId: String, - eventId: String?, - mxSession: MXSession, - threadParameters: ThreadParameters?, - presentationParameters: ScreenPresentationParameters + init(roomId: String, + eventId: String?, + mxSession: MXSession, + threadParameters: ThreadParameters?, + presentationParameters: ScreenPresentationParameters ) { - self.init(roomId: roomId, eventId: eventId, mxSession: mxSession, threadParameters: threadParameters, presentationParameters: presentationParameters, autoJoinInvitedRoom: false) + self.roomId = roomId + self.eventId = eventId + self.mxSession = mxSession + self.threadParameters = threadParameters + self.presentationParameters = presentationParameters + self.showSettingsInitially = false + self.senderId = nil + self.autoJoinInvitedRoom = false + + super.init() } init(roomId: String, diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index 3e8ec3044..a334926d1 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -1088,8 +1088,12 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro } else { - MXLogDebug(@"[RecentsViewController]: Reloading table view section %ld", indexPath.section); - [self.recentsTableView reloadSections:[NSIndexSet indexSetWithIndex:indexPath.section] withRowAnimation:UITableViewRowAnimationNone]; + // Ideally we would call tableView.reloadSections, but this can lead to crashes if multiple sections need such an update and they + // vertically depend on each other. It is unclear whether this is due to further issues in the data model (e.g. data race) + // or some undocumented table view behavior. To avoid this we reload the entire table view, even if this means reloading + // multiple times for several section updates. + MXLogDebug(@"[RecentsViewController]: Reloading the entire table view due to updates in section %ld", indexPath.section); + [self refreshRecentsTable]; } } else if (!changes) diff --git a/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift b/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift index c194d0b90..599b50c38 100644 --- a/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift +++ b/Riot/Modules/Spaces/SpaceList/SpaceListViewModel.swift @@ -116,18 +116,19 @@ final class SpaceListViewModel: SpaceListViewModelType { } func select(spaceWithId spaceId: String) { - for (sectionIndex, section) in self.sections.enumerated() { - switch section { - case .home: break - case .addSpace: break - case .spaces(let viewDataList): - for (row, itemViewData) in viewDataList.enumerated() where itemViewData.spaceId == spaceId { - let indexPath = IndexPath(row: row, section: sectionIndex) - self.selectSpace(with: spaceId) - self.selectedIndexPath = indexPath - self.update(viewState: .selectionChanged(indexPath)) - } - } + var foundIndexPath: IndexPath? + + if let spaceService = self.userSessionsService.mainUserSession?.matrixSession.spaceService, + let firstRootAncestor = spaceService.firstRootAncestorForRoom(withId: spaceId) { + foundIndexPath = indexPathOf(spaceWithId: firstRootAncestor.spaceId) + } else { + foundIndexPath = indexPathOf(spaceWithId: spaceId) + } + + if let indexPath = foundIndexPath { + self.selectSpace(with: spaceId) + self.selectedIndexPath = indexPath + self.update(viewState: .selectionChanged(indexPath)) } } @@ -303,4 +304,20 @@ final class SpaceListViewModel: SpaceListViewModelType { self.update(viewState: .loaded([])) } + + private func indexPathOf(spaceWithId spaceId: String) -> IndexPath? { + for (sectionIndex, section) in self.sections.enumerated() { + switch section { + case .home: break + case .addSpace: break + case .spaces(let viewDataList): + for (row, itemViewData) in viewDataList.enumerated() where itemViewData.spaceId == spaceId { + return IndexPath(row: row, section: sectionIndex) + } + } + } + + return nil + } + } diff --git a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift index dce516a80..ef3290ec7 100644 --- a/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift +++ b/Riot/Modules/Spaces/SpaceMembers/MemberList/SpaceMemberListViewController.swift @@ -142,7 +142,11 @@ final class SpaceMemberListViewController: RoomParticipantsViewController { private func renderLoaded(space: MXSpace) { self.activityPresenter.removeCurrentActivityIndicator(animated: true) self.mxRoom = space.room - self.titleView.subtitleLabel.text = space.summary?.displayname + if let spaceName = space.summary?.displayname { + self.titleView.breadcrumbView.breadcrumbs = [spaceName] + } else { + self.titleView.breadcrumbView.breadcrumbs = [] + } } private func render(error: Error) { diff --git a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift index 1fe801a1d..cccdd1860 100644 --- a/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift +++ b/Riot/Modules/Spaces/SpaceRoomList/ExploreRoom/SpaceExploreRoomViewController.swift @@ -190,7 +190,7 @@ final class SpaceExploreRoomViewController: UIViewController { case .loading: self.renderLoading() case .spaceNameFound(let spaceName): - self.titleView.subtitleLabel.text = spaceName + self.titleView.breadcrumbView.breadcrumbs = [spaceName] case .loaded(let children, let hasMore): self.hasMore = hasMore self.renderLoaded(children: children) diff --git a/Riot/Modules/TabBar/BreadcrumbView.swift b/Riot/Modules/TabBar/BreadcrumbView.swift new file mode 100644 index 000000000..bf0471e6d --- /dev/null +++ b/Riot/Modules/TabBar/BreadcrumbView.swift @@ -0,0 +1,128 @@ +// +// 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 Foundation +import UIKit + +/// `BreadcrumbView` can be used to display a path into a single line of text and manages ellipsis. +@objcMembers +class BreadcrumbView: UIView, Themable { + + // MARK: - Constants + + private enum Constants { + static let separator: String = "/" + } + + // MARK: - Properties + + public var breadcrumbs: [String] = [] { + didSet { + populateLabels() + } + } + + // MARK: - Private + + private var labels: [UILabel] = [] + + // MARK: - Lifecycle + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + // MARK: - Themable + + func update(theme: Theme) { + for label in labels { + update(theme: theme, for: label) + } + } + + // MARK: - Private + + private func populateLabels() { + for label in labels { + label.removeFromSuperview() + } + + labels.removeAll() + + for (index, breadcrumb) in breadcrumbs.enumerated() { + if index > 0 { + createLabel(with: Constants.separator, at: index) + } + createLabel(with: breadcrumb, at: index) + } + self.layoutIfNeeded() + } + + private func createLabel(with text: String?, at index: Int) { + guard let text = text, !text.isEmpty else { + return + } + + let label = UILabel(frame: .zero) + label.backgroundColor = .clear + label.text = text + let priority: UILayoutPriority + if index < breadcrumbs.count - 1 { + // We put a higher priority to the first element then decrease the priority linearly for the next elements. + priority = UILayoutPriority(UILayoutPriority.defaultLow.rawValue + Float(breadcrumbs.count * 2 - labels.count)) + } else { + // The last element has the highest priority + priority = .defaultHigh + } + label.setContentCompressionResistancePriority(priority, for: .horizontal) + + update(theme: ThemeService.shared().theme, for: label) + + self.addSubview(label) + self.labels.append(label) + + label.translatesAutoresizingMaskIntoConstraints = false + label.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor).isActive = true + label.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor).isActive = true + + if let prevSibling = prevSibling(of: label) { + label.leadingAnchor.constraint(equalTo: prevSibling.trailingAnchor).isActive = true + } else { + label.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor).isActive = true + } + + if index == breadcrumbs.count - 1 && label.text != Constants.separator { + label.trailingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.trailingAnchor).isActive = true + } + } + + private func prevSibling(of label: UILabel) -> UILabel? { + guard let index = labels.firstIndex(of: label), index - 1 >= 0 else { + return nil + } + + return labels[index-1] + } + + private func update(theme: Theme, for label: UILabel) { + label.textColor = theme.colors.tertiaryContent + label.font = theme.fonts.footnote + } +} diff --git a/Riot/Modules/TabBar/MainTitleView.swift b/Riot/Modules/TabBar/MainTitleView.swift index 701dc4d90..2194f542e 100644 --- a/Riot/Modules/TabBar/MainTitleView.swift +++ b/Riot/Modules/TabBar/MainTitleView.swift @@ -22,7 +22,7 @@ class MainTitleView: UIStackView, Themable { // MARK: - Properties public private(set) var titleLabel: UILabel! - public private(set) var subtitleLabel: UILabel! + public private(set) var breadcrumbView: BreadcrumbView! // MARK: - Lifecycle @@ -42,8 +42,7 @@ class MainTitleView: UIStackView, Themable { self.titleLabel.textColor = theme.colors.primaryContent self.titleLabel.font = theme.fonts.calloutSB - self.subtitleLabel.textColor = theme.colors.tertiaryContent - self.subtitleLabel.font = theme.fonts.footnote + self.breadcrumbView.update(theme: theme) } // MARK: - Private @@ -52,11 +51,10 @@ class MainTitleView: UIStackView, Themable { self.titleLabel = UILabel(frame: .zero) self.titleLabel.backgroundColor = .clear - self.subtitleLabel = UILabel(frame: .zero) - self.subtitleLabel.backgroundColor = .clear + self.breadcrumbView = BreadcrumbView(frame: .zero) self.addArrangedSubview(titleLabel) - self.addArrangedSubview(subtitleLabel) + self.addArrangedSubview(breadcrumbView) self.distribution = .equalCentering self.axis = .vertical self.alignment = .center diff --git a/Riot/Modules/TabBar/MasterTabBarController.m b/Riot/Modules/TabBar/MasterTabBarController.m index 435f7ebcb..91dab11c1 100644 --- a/Riot/Modules/TabBar/MasterTabBarController.m +++ b/Riot/Modules/TabBar/MasterTabBarController.m @@ -723,8 +723,24 @@ - (void)filterRoomsWithParentId:(NSString*)roomParentId inMatrixSession:(MXSession*)mxSession { - titleView.subtitleLabel.text = roomParentId ? [mxSession roomSummaryWithRoomId:roomParentId].displayname : nil; + if (roomParentId) { + NSString *parentName = [mxSession roomSummaryWithRoomId:roomParentId].displayname; + NSMutableArray *breadcrumbs = [[NSMutableArray alloc] initWithObjects:parentName, nil]; + MXSpace *firstRootAncestor = roomParentId ? [mxSession.spaceService firstRootAncestorForRoomWithId:roomParentId] : nil; + NSString *rootName = nil; + if (firstRootAncestor) + { + rootName = [mxSession roomSummaryWithRoomId:firstRootAncestor.spaceId].displayname; + [breadcrumbs insertObject:rootName atIndex:0]; + } + titleView.breadcrumbView.breadcrumbs = breadcrumbs; + } + else + { + titleView.breadcrumbView.breadcrumbs = @[]; + } + recentsDataSource.currentSpace = [mxSession.spaceService getSpaceWithId:roomParentId]; [self updateSideMenuNotifcationIcon]; } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift index b9c69dc10..b4ade5e31 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift @@ -25,12 +25,20 @@ enum LocationSharingCoordinateType { case pin } +enum LiveLocationSharingTimeout: TimeInterval { + // Timer are in milliseconde because timestamp are in millisecond in Matrix SDK + case short = 900000 // 15 minutes + case medium = 3600000 // 1 hour + case long = 28800000 // 8 hours +} + enum LocationSharingViewAction { case cancel case share case sharePinLocation case goToUserLocation - case shareLiveLocation + case startLiveSharing + case shareLiveLocation(timeout: LiveLocationSharingTimeout) } enum LocationSharingViewModelResult { @@ -87,6 +95,7 @@ struct LocationSharingViewStateBindings { var alertInfo: AlertInfo? var userLocation: CLLocationCoordinate2D? var pinLocation: CLLocationCoordinate2D? + var showingTimerSelector = false } enum LocationSharingAlertType { diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift index e6a7f6226..e8429ec34 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift @@ -25,12 +25,6 @@ typealias LocationSharingViewModelType = StateStoreViewModel