From 74c6ba22e2bdba553cfc4546d6d943d827cedf57 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 5 Apr 2022 17:26:24 +0200 Subject: [PATCH 001/158] Add DSBottomSheet and update iOS min version to iOS 13. --- Podfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Podfile b/Podfile index ec5c0420d..2f52cdd72 100644 --- a/Podfile +++ b/Podfile @@ -1,7 +1,7 @@ source 'https://cdn.cocoapods.org/' # Uncomment this line to define a global platform for your project -platform :ios, '12.1' +platform :ios, '13.0' # Use frameworks to allow usage of pods written in Swift use_frameworks! @@ -57,6 +57,7 @@ end def import_SwiftUI_pods pod 'Introspect', '~> 0.1' + pod 'DSBottomSheet', '~> 0.3' end abstract_target 'RiotPods' do @@ -154,4 +155,4 @@ post_install do |installer| config.build_settings['OTHER_SWIFT_FLAGS'] ||= ['$(inherited)', '-Xcc', '-Wno-nullability-completeness'] end end -end \ No newline at end of file +end From a2cad415587901230c0b5989fb83e3278772025a Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 5 Apr 2022 17:30:17 +0200 Subject: [PATCH 002/158] Create map callout view UserAnnotationCalloutContentView. --- .../UserAnnotationCalloutContentView.swift | 85 +++++++++++++++++++ .../View/UserAnnotationCalloutContentView.xib | 65 ++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutContentView.swift create mode 100644 RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutContentView.xib diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutContentView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutContentView.swift new file mode 100644 index 000000000..419213972 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutContentView.swift @@ -0,0 +1,85 @@ +// +// 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 Reusable + +class UserAnnotationCalloutContentView: UIView, Themable, NibLoadable { + + // MARK: - Constants + + private static let sizingView = UserAnnotationCalloutContentView.instantiate() + + private enum Constants { + static let height: CGFloat = 44.0 + static let cornerRadius: CGFloat = 8.0 + } + + // MARK: - Properties + + // MARK: Outlets + + @IBOutlet var backgroundView: UIView! + @IBOutlet var titleLabel: UILabel! + @IBOutlet var shareButton: UIButton! + + // MARK: - Setup + + static func instantiate() -> UserAnnotationCalloutContentView { + return UserAnnotationCalloutContentView.loadFromNib() + } + + // MARK: - Public + + func update(theme: Theme) { + self.backgroundView.backgroundColor = theme.colors.background + self.titleLabel.textColor = theme.colors.secondaryContent + self.titleLabel.font = theme.fonts.callout + self.shareButton.tintColor = theme.colors.secondaryContent + } + + // MARK: - Life cycle + + override func awakeFromNib() { + super.awakeFromNib() + + self.titleLabel.text = VectorL10n.locationSharingLiveMapCalloutTitle + self.backgroundView.layer.masksToBounds = true + } + + override func layoutSubviews() { + super.layoutSubviews() + + self.backgroundView.layer.cornerRadius = Constants.cornerRadius + } + + static func contentViewSize() -> CGSize { + let sizingView = self.sizingView + + sizingView.frame = CGRect(x: 0, y: 0, width: 1, height: Constants.height) + + sizingView.setNeedsLayout() + sizingView.layoutIfNeeded() + + let fittingSize = CGSize(width: UIView.layoutFittingCompressedSize.width, height: Constants.height) + + let size = sizingView.systemLayoutSizeFitting(fittingSize, + withHorizontalFittingPriority: .fittingSizeLevel, + verticalFittingPriority: .required) + + return size + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutContentView.xib b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutContentView.xib new file mode 100644 index 000000000..8b438cac2 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutContentView.xib @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From d7306f60994cedff0735e0735a6491abcb1969bf Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 5 Apr 2022 17:43:34 +0200 Subject: [PATCH 003/158] Create map callout view UserAnnotationCalloutView. --- .../View/UserAnnotationCalloutView.swift | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutView.swift diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutView.swift new file mode 100644 index 000000000..f770b2bec --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutView.swift @@ -0,0 +1,156 @@ +// +// 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 Mapbox + +class UserAnnotationCalloutView: UIView, MGLCalloutView, Themable { + + // MARK: - Constants + + private enum Constants { + static let animationDuration: TimeInterval = 0.2 + static let bottomMargin: CGFloat = 3.0 + } + + // MARK: - Properties + + // MARK: Overrides + + var representedObject: MGLAnnotation + + lazy var leftAccessoryView: UIView = UIView() + + lazy var rightAccessoryView: UIView = UIView() + + var delegate: MGLCalloutViewDelegate? + + // Allow the callout to remain open during panning. + let dismissesAutomatically: Bool = false + + let isAnchoredToAnnotation: Bool = true + + // https://github.com/mapbox/mapbox-gl-native/issues/9228 + override var center: CGPoint { + set { + var newCenter = newValue + newCenter.y -= bounds.maxY + Constants.bottomMargin + super.center = newCenter + } + get { + return super.center + } + } + + // MARK: Private + + lazy var contentView: UserAnnotationCalloutContentView = { + return UserAnnotationCalloutContentView.instantiate() + }() + + // MARK: - Setup + + required init(userLocationAnnotation: UserLocationAnnotation) { + + self.representedObject = userLocationAnnotation + + super.init(frame: .zero) + + self.vc_addSubViewMatchingParent(self.contentView) + + self.update(theme: ThemeService.shared().theme) + + let size = UserAnnotationCalloutContentView.contentViewSize() + + self.frame = CGRect(origin: .zero, size: size) + } + + required init?(coder decoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Public + + func update(theme: Theme) { + self.contentView.update(theme: theme) + } + + // MARK: - Overrides + + func presentCallout(from rect: CGRect, in view: UIView, constrainedTo constrainedRect: CGRect, animated: Bool) { + + // Set callout above the marker view + + self.center = view.center.applying(CGAffineTransform(translationX: 0, y: view.bounds.height/2 + self.bounds.height)) + + delegate?.calloutViewWillAppear?(self) + + view.addSubview(self) + + if isCalloutTappable() { + // Handle taps and eventually try to send them to the delegate (usually the map view). + self.contentView.shareButton.addTarget(self, action: #selector(CustomCalloutView.calloutTapped), for: .touchUpInside) + } else { + // Disable tapping and highlighting. + self.contentView.shareButton.isUserInteractionEnabled = false + } + + if animated { + alpha = 0 + + UIView.animate(withDuration: Constants.animationDuration) { [weak self] in + guard let strongSelf = self else { + return + } + + strongSelf.alpha = 1 + strongSelf.delegate?.calloutViewDidAppear?(strongSelf) + } + } else { + delegate?.calloutViewDidAppear?(self) + } + } + + func dismissCallout(animated: Bool) { + if (superview != nil) { + if animated { + UIView.animate(withDuration: Constants.animationDuration, animations: { [weak self] in + self?.alpha = 0 + }, completion: { [weak self] _ in + self?.removeFromSuperview() + }) + } else { + removeFromSuperview() + } + } + } + + // MARK: - Callout interaction handlers + + func isCalloutTappable() -> Bool { + if let delegate = delegate { + if delegate.responds(to: #selector(MGLCalloutViewDelegate.calloutViewShouldHighlight)) { + return delegate.calloutViewShouldHighlight!(self) + } + } + return false + } + + @objc func calloutTapped() { + if isCalloutTappable() && delegate!.responds(to: #selector(MGLCalloutViewDelegate.calloutViewTapped)) { + delegate!.calloutViewTapped!(self) + } + } +} From 5bd5cd2baabe19ae22450ff95fedef445b8b06f2 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 5 Apr 2022 17:44:04 +0200 Subject: [PATCH 004/158] Update strings --- Riot/Assets/en.lproj/Vector.strings | 2 ++ Riot/Generated/Strings.swift | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index cfff2829a..2cfb16062 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2110,6 +2110,8 @@ Tap the + to start adding people."; "live_location_sharing_banner_stop" = "Stop"; "location_sharing_static_share_title" = "Send my current location"; "location_sharing_pin_drop_share_title" = "Send this location"; +"location_sharing_live_map_callout_title" = "Share location"; + // MARK: - MatrixKit diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index a09e12287..a5695e27b 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2759,6 +2759,10 @@ public class VectorL10n: NSObject { public static var locationSharingInvalidAuthorizationSettings: String { return VectorL10n.tr("Vector", "location_sharing_invalid_authorization_settings") } + /// Share location + public static var locationSharingLiveMapCalloutTitle: String { + return VectorL10n.tr("Vector", "location_sharing_live_map_callout_title") + } /// Share live location public static var locationSharingLiveShareTitle: String { return VectorL10n.tr("Vector", "location_sharing_live_share_title") From 9183d933e8b01c34ebc5d96d9545badcc2f54eb9 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 5 Apr 2022 17:47:04 +0200 Subject: [PATCH 005/158] LocationSharingMapView: Handle callout view for user annotation. --- .../View/LocationSharingMapView.swift | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift index 5828e5d0c..cb39e59e7 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift @@ -43,6 +43,9 @@ struct LocationSharingMapView: UIViewRepresentable { /// True to indicate to show and follow current user location var showsUserLocation: Bool = false + + /// True to indicate that a touch on user annotation can show a callout + var userAnnotationCanShowCallout: Bool = false /// Last user location if `showsUserLocation` has been enabled @Binding var userLocation: CLLocationCoordinate2D? @@ -50,6 +53,9 @@ struct LocationSharingMapView: UIViewRepresentable { /// Coordinate of the center of the map @Binding var mapCenterCoordinate: CLLocationCoordinate2D? + /// Called when an annotation callout view is tapped + var onCalloutTap: ((MGLAnnotation) -> Void)? + /// Publish view errors if any let errorSubject: PassthroughSubject @@ -160,6 +166,27 @@ extension LocationSharingMapView { } locationSharingMapView.mapCenterCoordinate = mapCenterCoordinate } + + // MARK: Callout + + func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool { + return annotation is UserLocationAnnotation && locationSharingMapView.userAnnotationCanShowCallout + } + + func mapView(_ mapView: MGLMapView, calloutViewFor annotation: MGLAnnotation) -> MGLCalloutView? { + if let userLocationAnnotation = annotation as? UserLocationAnnotation { + return UserAnnotationCalloutView(userLocationAnnotation: userLocationAnnotation) + } + + return nil + } + + func mapView(_ mapView: MGLMapView, tapOnCalloutFor annotation: MGLAnnotation) { + + locationSharingMapView.onCalloutTap?(annotation) + // Hide the callout + mapView.deselectAnnotation(annotation, animated: true) + } } } From 35035973315e59e3eb11641a98541b47251649d5 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 5 Apr 2022 17:51:18 +0200 Subject: [PATCH 006/158] Fix UserAnnotationCalloutView --- .../Room/LocationSharing/View/UserAnnotationCalloutView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutView.swift index f770b2bec..50721c6fa 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutView.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutView.swift @@ -101,7 +101,7 @@ class UserAnnotationCalloutView: UIView, MGLCalloutView, Themable { if isCalloutTappable() { // Handle taps and eventually try to send them to the delegate (usually the map view). - self.contentView.shareButton.addTarget(self, action: #selector(CustomCalloutView.calloutTapped), for: .touchUpInside) + self.contentView.shareButton.addTarget(self, action: #selector(UserAnnotationCalloutView.calloutTapped), for: .touchUpInside) } else { // Disable tapping and highlighting. self.contentView.shareButton.isUserInteractionEnabled = false From 3875101cf21920248e8e432999bdb886ef6268f4 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 5 Apr 2022 17:51:47 +0200 Subject: [PATCH 007/158] Create MapViewErrorAlertInfoBuilder --- .../MapViewErrorAlertInfoBuilder.swift | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 RiotSwiftUI/Modules/Room/LocationSharing/MapViewErrorAlertInfoBuilder.swift diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/MapViewErrorAlertInfoBuilder.swift b/RiotSwiftUI/Modules/Room/LocationSharing/MapViewErrorAlertInfoBuilder.swift new file mode 100644 index 000000000..9f1120d2a --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/MapViewErrorAlertInfoBuilder.swift @@ -0,0 +1,46 @@ +// +// 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 + +struct MapViewErrorAlertInfoBuilder { + + func build(with error: LocationSharingViewError, primaryButtonCompletion: (() -> Void)?) -> AlertInfo? { + + let alertInfo: AlertInfo? + + switch error { + case .failedLoadingMap: + alertInfo = AlertInfo(id: .mapLoadingError, + title: VectorL10n.locationSharingLoadingMapErrorTitle(AppInfo.current.displayName), + primaryButton: (VectorL10n.ok, primaryButtonCompletion)) + case .failedLocatingUser: + alertInfo = AlertInfo(id: .userLocatingError, + title: VectorL10n.locationSharingLocatingUserErrorTitle(AppInfo.current.displayName), + primaryButton: (VectorL10n.ok, primaryButtonCompletion)) + case .invalidLocationAuthorization: + alertInfo = AlertInfo(id: .authorizationError, + title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName), + primaryButton: (VectorL10n.locationSharingInvalidAuthorizationNotNow, primaryButtonCompletion), + secondaryButton: (VectorL10n.locationSharingInvalidAuthorizationSettings, primaryButtonCompletion)) + default: + alertInfo = nil + } + + return alertInfo + } + +} From 4f020de51903e8a1d49bbebfae38d46e800b9fa2 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 5 Apr 2022 17:56:02 +0200 Subject: [PATCH 008/158] Prepare LiveLocationSharingViewerService and handle mock. --- ...LocationSharingViewerServiceProtocol.swift | 27 ++++++ .../LiveLocationSharingViewerService.swift | 42 +++++++++ ...MockLiveLocationSharingViewerService.swift | 93 +++++++++++++++++++ .../Service/UserLiveLocation.swift | 43 +++++++++ 4 files changed, 205 insertions(+) create mode 100644 RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Service/LiveLocationSharingViewerServiceProtocol.swift create mode 100644 RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Service/MatrixSDK/LiveLocationSharingViewerService.swift create mode 100644 RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Service/Mock/MockLiveLocationSharingViewerService.swift create mode 100644 RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Service/UserLiveLocation.swift diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Service/LiveLocationSharingViewerServiceProtocol.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Service/LiveLocationSharingViewerServiceProtocol.swift new file mode 100644 index 000000000..8d272fb0a --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Service/LiveLocationSharingViewerServiceProtocol.swift @@ -0,0 +1,27 @@ +// +// Copyright 2021 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 Combine +import CoreLocation + +@available(iOS 14.0, *) +protocol LiveLocationSharingViewerServiceProtocol { + + var usersLiveLocation: [UserLiveLocation] { get } + + func isCurrentUserId(_ userId: String) -> Bool +} diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Service/MatrixSDK/LiveLocationSharingViewerService.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Service/MatrixSDK/LiveLocationSharingViewerService.swift new file mode 100644 index 000000000..65a7da476 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Service/MatrixSDK/LiveLocationSharingViewerService.swift @@ -0,0 +1,42 @@ +// +// Copyright 2021 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 CoreLocation + +@available(iOS 14.0, *) +class LiveLocationSharingViewerService: LiveLocationSharingViewerServiceProtocol { + + // MARK: - Properties + + private(set) var usersLiveLocation: [UserLiveLocation] = [] + + // MARK: Private + + private let session: MXSession + + // MARK: Public + + func isCurrentUserId(_ userId: String) -> Bool { + return false + } + + // MARK: - Setup + + init(session: MXSession) { + self.session = session + } +} diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Service/Mock/MockLiveLocationSharingViewerService.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Service/Mock/MockLiveLocationSharingViewerService.swift new file mode 100644 index 000000000..c95e5c571 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Service/Mock/MockLiveLocationSharingViewerService.swift @@ -0,0 +1,93 @@ +// +// Copyright 2021 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 Combine +import CoreLocation + +@available(iOS 14.0, *) +class MockLiveLocationSharingViewerService: LiveLocationSharingViewerServiceProtocol { + + private(set) var usersLiveLocation: [UserLiveLocation] = [] + + func isCurrentUserId(_ userId: String) -> Bool { + return "@alice:matrix.org" == userId + } + + init(generateRandomUsers: Bool = false) { + + let firstUserLiveLocation = self.createFirstUserLiveLocation() + + let secondUserLiveLocation = self.createSecondUserLiveLocation() + + var usersLiveLocation: [UserLiveLocation] = [firstUserLiveLocation, secondUserLiveLocation] + + + if generateRandomUsers { + for _ in 1...20 { + let randomUser = self.createRandomUserLiveLocation() + usersLiveLocation.append(randomUser) + } + } + + self.usersLiveLocation = usersLiveLocation + } + + private func createFirstUserLiveLocation() -> UserLiveLocation { + let userAvatarData = AvatarInput(mxContentUri: nil, matrixItemId: "@alice:matrix.org", displayName: "Alice") + let userCoordinate = CLLocationCoordinate2D(latitude: 51.4932641, longitude: -0.257096) + + let currentTimeInterval = Date().timeIntervalSince1970 + let timestamp = currentTimeInterval - 300 + let timeout: TimeInterval = 800 + let lastUpdate = currentTimeInterval - 100 + + return UserLiveLocation(avatarData: userAvatarData, timestamp: timestamp, timeout: timeout, lastUpdate: lastUpdate, coordinate: userCoordinate) + } + + private func createSecondUserLiveLocation() -> UserLiveLocation { + + let userAvatarData = AvatarInput(mxContentUri: nil, matrixItemId: "@bob:matrix.org", displayName: "Bob") + let coordinate = CLLocationCoordinate2D(latitude: 51.4952641, longitude: -0.259096) + + let currentTimeInterval = Date().timeIntervalSince1970 + + let timestamp = currentTimeInterval - 600 + let timeout: TimeInterval = 1200 + let lastUpdate = currentTimeInterval - 300 + + return UserLiveLocation(avatarData: userAvatarData, timestamp: timestamp, timeout: timeout, lastUpdate: lastUpdate, coordinate: coordinate) + } + + + private func createRandomUserLiveLocation() -> UserLiveLocation { + + let uuidString = UUID().uuidString.suffix(8) + + let random = Double.random(in: 0.005...0.010) + + let userAvatarData = AvatarInput(mxContentUri: nil, matrixItemId: "@user_\(uuidString):matrix.org", displayName: "User \(uuidString)") + let coordinate = CLLocationCoordinate2D(latitude: 51.4952641 + random, longitude: -0.259096 + random) + + let currentTimeInterval = Date().timeIntervalSince1970 + + let timestamp = currentTimeInterval - 600 + let timeout: TimeInterval = 1200 + let lastUpdate = currentTimeInterval - 300 + + return UserLiveLocation(avatarData: userAvatarData, timestamp: timestamp, timeout: timeout, lastUpdate: lastUpdate, coordinate: coordinate) + } +} diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Service/UserLiveLocation.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Service/UserLiveLocation.swift new file mode 100644 index 000000000..015d09088 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Service/UserLiveLocation.swift @@ -0,0 +1,43 @@ +// +// 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 CoreLocation + +/// Represents user live location +struct UserLiveLocation { + + var userId: String { + return avatarData.matrixItemId + } + + var displayName: String { + return avatarData.displayName ?? self.userId + } + + let avatarData: AvatarInputProtocol + + /// Location sharing start date + let timestamp: TimeInterval + + /// Sharing duration from the start sharing date + let timeout: TimeInterval + + /// Last coordinatore update date + let lastUpdate: TimeInterval + + let coordinate: CLLocationCoordinate2D +} From c47bd5ea25dc20e6345d0dc5e8fc26531c710371 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 5 Apr 2022 18:19:39 +0200 Subject: [PATCH 009/158] Handle LiveLocationListItem. --- .../View/LiveLocationListItem.swift | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationListItem.swift diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationListItem.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationListItem.swift new file mode 100644 index 000000000..c5f4b94d6 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationListItem.swift @@ -0,0 +1,192 @@ +// +// 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 + +@available(iOS 14.0, *) +struct LiveLocationListItem: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + // MARK: Public + + let viewData: LiveLocationListItemViewData + + var timeoutText: String { + + let timeLeftString: String + + if let elapsedTimeString = self.elapsedTimeString(from: viewData.expirationDate, isPastDate: false) { + timeLeftString = VectorL10n.locationSharingLiveListItemTimeLeft(elapsedTimeString) + } else { + timeLeftString = VectorL10n.locationSharingLiveListItemSharingExpired + } + + return timeLeftString + } + + var lastUpdateText: String { + + let timeLeftString: String + + if let elapsedTimeString = self.elapsedTimeString(from: viewData.lastUpdate, isPastDate: true) { + timeLeftString = VectorL10n.locationSharingLiveListItemLastUpdate(elapsedTimeString) + } else { + timeLeftString = VectorL10n.locationSharingLiveListItemLastUpdateInvalid + } + + return timeLeftString + } + + var displayName: String { + return viewData.isCurrentUser ? VectorL10n.locationSharingLiveListItemCurrentUserDisplayName : viewData.displayName + } + + var onStopSharingAction: (() -> (Void))? = nil + + var onBackgroundTap: ((String) -> (Void))? = nil + + // MARK: - Body + + var body: some View { + HStack { + HStack(spacing: 18) { + AvatarImage(avatarData: viewData.avatarData, size: .medium) + .border() + VStack(alignment: .leading, spacing: 2) { Text(displayName) + .font(theme.fonts.bodySB) + .foregroundColor(theme.colors.primaryContent) + Text(timeoutText) + .font(theme.fonts.caption1) + .foregroundColor(theme.colors.primaryContent) + Text(lastUpdateText) + .font(theme.fonts.caption1) + .foregroundColor(theme.colors.secondaryContent) + } + } + if viewData.isCurrentUser { + Spacer() + Button(VectorL10n.locationSharingLiveListItemStopSharingAction) { + onStopSharingAction?() + } + .font(theme.fonts.caption1) + .foregroundColor(theme.colors.alert) + } + } + .onTapGesture { + onBackgroundTap?(self.viewData.userId) + } + } + + // MARK: - Private + + private func elapsedTimeString(from timestamp: TimeInterval, isPastDate: Bool) -> String? { + + let formatter = DateComponentsFormatter() + + formatter.unitsStyle = .abbreviated + formatter.allowedUnits = [.hour, .minute] + + let date = Date(timeIntervalSince1970: timestamp) + + let elaspedTimeinterval = date.timeIntervalSinceNow + + var timeLeftString: String? + + // Negative value indicate that the timestamp is in the past + // Positive value indicate that the timestamp is in the future + // Return nil if the sign is not the one as expected + if (isPastDate && elaspedTimeinterval <= 0) || (!isPastDate && elaspedTimeinterval >= 0) { + timeLeftString = formatter.string(from: abs(elaspedTimeinterval)) + } + + return timeLeftString + } +} + +@available(iOS 14.0, *) +struct LiveLocationListPreview: View { + + let liveLocationSharingViewerService: LiveLocationSharingViewerServiceProtocol = MockLiveLocationSharingViewerService() + + var viewDataList: [LiveLocationListItemViewData] { + return self.listItemsViewData(from: liveLocationSharingViewerService.usersLiveLocation) + } + + var body: some View { + VStack(alignment: .leading, spacing: 14) { + ForEach(viewDataList) { viewData in + LiveLocationListItem(viewData: viewData, onStopSharingAction: { + + }, onBackgroundTap: { userId in + + }) + } + Spacer() + } + .padding() + } + + private func listItemsViewData(from usersLiveLocation: [UserLiveLocation]) -> [LiveLocationListItemViewData] { + + var listItemsViewData: [LiveLocationListItemViewData] = [] + + let sortedUsersLiveLocation = usersLiveLocation.sorted { userLiveLocation1, userLiveLocation2 in + return userLiveLocation1.displayName > userLiveLocation2.displayName + } + + listItemsViewData = sortedUsersLiveLocation.map({ userLiveLocation in + return self.listItemViewData(from: userLiveLocation) + }) + + let currentUserIndex = listItemsViewData.firstIndex { viewData in + return viewData.isCurrentUser + } + + // Move current user as first item + if let currentUserIndex = currentUserIndex { + + let currentUserViewData = listItemsViewData[currentUserIndex] + listItemsViewData.remove(at: currentUserIndex) + listItemsViewData.insert(currentUserViewData, at: 0) + } + + return listItemsViewData + } + + private func listItemViewData(from userLiveLocation: UserLiveLocation) -> LiveLocationListItemViewData { + + let isCurrentUser = self.liveLocationSharingViewerService.isCurrentUserId(userLiveLocation.userId) + + let expirationDate = userLiveLocation.timestamp + userLiveLocation.timeout + + return LiveLocationListItemViewData(userId: userLiveLocation.userId, isCurrentUser: isCurrentUser, avatarData: userLiveLocation.avatarData, displayName: userLiveLocation.displayName, expirationDate: expirationDate, lastUpdate: userLiveLocation.lastUpdate) + } +} + +@available(iOS 14.0, *) +struct LiveLocationListItem_Previews: PreviewProvider { + static var previews: some View { + Group { + LiveLocationListPreview().theme(.light).preferredColorScheme(.light) + LiveLocationListPreview().theme(.dark).preferredColorScheme(.dark) + } + } +} From 3eaf95750d4c294604684c487e593865a46a0f88 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 5 Apr 2022 18:25:42 +0200 Subject: [PATCH 010/158] Add LiveLocationListItemViewData view data for LiveLocationListItem. --- .../View/LiveLocationListItemViewData.swift | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationListItemViewData.swift diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationListItemViewData.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationListItemViewData.swift new file mode 100644 index 000000000..3e2392225 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationListItemViewData.swift @@ -0,0 +1,39 @@ +// +// 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 + +/// View data for LiveLocationListItem +struct LiveLocationListItemViewData: Identifiable { + + var id: String { + return userId + } + + let userId: String + + let isCurrentUser: Bool + + let avatarData: AvatarInputProtocol + + let displayName: String + + /// The location sharing expiration date + let expirationDate: TimeInterval + + /// Last coordinatore update + let lastUpdate: TimeInterval +} From 9798cae6faa75425d83c935e71742cfe6c1f67e6 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 5 Apr 2022 18:26:20 +0200 Subject: [PATCH 011/158] Add ShareLocationActivityControllerBuilder to build UIActivityViewController to share a location. --- .../ShareLocationActivityController.swift | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/ShareLocationActivityController.swift diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/ShareLocationActivityController.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/ShareLocationActivityController.swift new file mode 100644 index 000000000..d7e75020e --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/ShareLocationActivityController.swift @@ -0,0 +1,29 @@ +// +// 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 CoreLocation + +/// Build a UIActivityViewController to share a location +class ShareLocationActivityControllerBuilder { + + func build(with location: CLLocationCoordinate2D) -> UIActivityViewController { + return UIActivityViewController(activityItems: [ShareToMapsAppActivity.urlForMapsAppType(.apple, location: location)], + applicationActivities: [ShareToMapsAppActivity(type: .apple, location: location), + ShareToMapsAppActivity(type: .google, location: location), + ShareToMapsAppActivity(type: .osm, location: location)]) + } +} From b15cf58e69458643a69b160cb8bf55296180b7e1 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 5 Apr 2022 18:36:41 +0200 Subject: [PATCH 012/158] Handle live location sharing viewer screen. --- .../Modules/Common/Mock/MockAppScreens.swift | 1 + ...LiveLocationSharingViewerCoordinator.swift | 87 +++++++++ .../LiveLocationSharingViewerModels.swift | 68 +++++++ .../LiveLocationSharingViewerViewModel.swift | 181 ++++++++++++++++++ ...cationSharingViewerViewModelProtocol.swift | 24 +++ ...LiveLocationSharingViewerScreenState.swift | 63 ++++++ .../UI/LiveLocationSharingViewerUITests.swift | 26 +++ ...eLocationSharingViewerViewModelTests.swift | 35 ++++ .../View/LiveLocationSharingViewer.swift | 118 ++++++++++++ 9 files changed, 603 insertions(+) create mode 100644 RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Coordinator/LiveLocationSharingViewerCoordinator.swift create mode 100644 RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/LiveLocationSharingViewerModels.swift create mode 100644 RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/LiveLocationSharingViewerViewModel.swift create mode 100644 RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/LiveLocationSharingViewerViewModelProtocol.swift create mode 100644 RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/MockLiveLocationSharingViewerScreenState.swift create mode 100644 RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Test/UI/LiveLocationSharingViewerUITests.swift create mode 100644 RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Test/Unit/LiveLocationSharingViewerViewModelTests.swift create mode 100644 RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift index cae73c224..8611b3e4c 100644 --- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift +++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift @@ -20,6 +20,7 @@ import Foundation @available(iOS 14.0, *) enum MockAppScreens { static let appScreens: [MockScreenState.Type] = [ + MockLiveLocationSharingViewerScreenState.self, MockOnboardingCelebrationScreenState.self, MockOnboardingAvatarScreenState.self, MockOnboardingDisplayNameScreenState.self, diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Coordinator/LiveLocationSharingViewerCoordinator.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Coordinator/LiveLocationSharingViewerCoordinator.swift new file mode 100644 index 000000000..1f9d74002 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Coordinator/LiveLocationSharingViewerCoordinator.swift @@ -0,0 +1,87 @@ +// +// Copyright 2021 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 LiveLocationSharingViewerCoordinatorParameters { + let session: MXSession +} + +final class LiveLocationSharingViewerCoordinator: Coordinator, Presentable { + + // MARK: - Properties + + // MARK: Private + + private let parameters: LiveLocationSharingViewerCoordinatorParameters + private let liveLocationSharingViewerHostingController: UIViewController + private var liveLocationSharingViewerViewModel: LiveLocationSharingViewerViewModelProtocol + + private let shareLocationActivityControllerBuilder = ShareLocationActivityControllerBuilder() + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + var completion: (() -> Void)? + + // MARK: - Setup + + @available(iOS 14.0, *) + init(parameters: LiveLocationSharingViewerCoordinatorParameters) { + self.parameters = parameters + + let service = LiveLocationSharingViewerService(session: parameters.session) + + let viewModel = LiveLocationSharingViewerViewModel(mapStyleURL: BuildSettings.tileServerMapStyleURL, service: service) + let view = LiveLocationSharingViewer(viewModel: viewModel.context) + .addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager)) + liveLocationSharingViewerViewModel = viewModel + liveLocationSharingViewerHostingController = VectorHostingController(rootView: view) + } + + // MARK: - Public + func start() { + MXLog.debug("[LiveLocationSharingViewerCoordinator] did start.") + liveLocationSharingViewerViewModel.completion = { [weak self] result in + guard let self = self else { return } + MXLog.debug("[LiveLocationSharingViewerCoordinator] LiveLocationSharingViewerViewModel did complete with result: \(result).") + switch result { + case .done: + self.completion?() + case .share(let coordinate): + self.presentLocationActivityController(with: coordinate) + case .stopLocationSharing: + self.stopLocationSharing() + } + } + } + + func toPresentable() -> UIViewController { + return self.liveLocationSharingViewerHostingController + } + + func presentLocationActivityController(with coordinate: CLLocationCoordinate2D) { + + let shareActivityController = shareLocationActivityControllerBuilder.build(with: coordinate) + + self.liveLocationSharingViewerHostingController.present(shareActivityController, animated: true) + } + + func stopLocationSharing() { + // TODO: Handle stop location sharing + } +} diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/LiveLocationSharingViewerModels.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/LiveLocationSharingViewerModels.swift new file mode 100644 index 000000000..018aeee75 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/LiveLocationSharingViewerModels.swift @@ -0,0 +1,68 @@ +// +// Copyright 2021 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 Combine +import CoreLocation + +// MARK: - Coordinator + +// MARK: View model + +enum LiveLocationSharingViewerViewModelResult { + case done + case share(_ coordinate: CLLocationCoordinate2D) + case stopLocationSharing +} + +// MARK: View + +@available(iOS 14, *) +struct LiveLocationSharingViewerViewState: BindableState { + + /// Map style URL + let mapStyleURL: URL + + /// Map annotations to display on map + var annotations: [UserLocationAnnotation] + + /// Map annotation to focus on + var highlightedAnnotation: UserLocationAnnotation? + + /// Live location list items + var listItemsViewData: [LiveLocationListItemViewData] + + var showLoadingIndicator: Bool = false + + var shareButtonEnabled: Bool { + !showLoadingIndicator + } + + let errorSubject = PassthroughSubject() + + var bindings = LocationSharingViewStateBindings() +} + +struct LiveLocationSharingViewerViewStateBindings { + var alertInfo: AlertInfo? +} + +enum LiveLocationSharingViewerViewAction { + case done + case stopSharing + case tapListItem(_ userId: String) + case share(_ annotation: UserLocationAnnotation) +} diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/LiveLocationSharingViewerViewModel.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/LiveLocationSharingViewerViewModel.swift new file mode 100644 index 000000000..9d8a31dc9 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/LiveLocationSharingViewerViewModel.swift @@ -0,0 +1,181 @@ +// +// Copyright 2021 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 +import Combine +import Mapbox + +@available(iOS 14, *) +typealias LiveLocationSharingViewerViewModelType = StateStoreViewModel +@available(iOS 14, *) +class LiveLocationSharingViewerViewModel: LiveLocationSharingViewerViewModelType, LiveLocationSharingViewerViewModelProtocol { + + // MARK: - Properties + + // MARK: Private + + private let liveLocationSharingViewerService: LiveLocationSharingViewerServiceProtocol + + private var mapViewErrorAlertInfoBuilder: MapViewErrorAlertInfoBuilder + + // MARK: Public + + var completion: ((LiveLocationSharingViewerViewModelResult) -> Void)? + + // MARK: - Setup + + init(mapStyleURL: URL, service: LiveLocationSharingViewerServiceProtocol) { + + let viewState = LiveLocationSharingViewerViewState(mapStyleURL: mapStyleURL, annotations: [], highlightedAnnotation: nil, listItemsViewData: []) + + liveLocationSharingViewerService = service + mapViewErrorAlertInfoBuilder = MapViewErrorAlertInfoBuilder() + + super.init(initialViewState: viewState) + + state.errorSubject.sink { [weak self] error in + guard let self = self else { return } + self.processError(error) + }.store(in: &cancellables) + + self.update(with: service.usersLiveLocation) + } + + // MARK: - Public + + override func process(viewAction: LiveLocationSharingViewerViewAction) { + switch viewAction { + case .done: + completion?(.done) + case .stopSharing: + completion?(.stopLocationSharing) + case .tapListItem(let userId): + self.highlighAnnotation(with: userId) + case .share(let userLocationAnnotation): + completion?(.share(userLocationAnnotation.coordinate)) + } + } + + // MARK: - Private + + private func processError(_ error: LocationSharingViewError) { + guard state.bindings.alertInfo == nil else { + return + } + + let alertInfo = mapViewErrorAlertInfoBuilder.build(with: error) { [weak self] in + + switch error { + case .invalidLocationAuthorization: + if let applicationSettingsURL = URL(string:UIApplication.openSettingsURLString) { + UIApplication.shared.open(applicationSettingsURL) + } else { + self?.completion?(.done) + } + default: + self?.completion?(.done) + } + } + + state.bindings.alertInfo = alertInfo + } + + private func userLocationAnnotations(from usersLiveLocation: [UserLiveLocation]) -> [UserLocationAnnotation] { + + return usersLiveLocation.map { userLiveLocation in + return UserLocationAnnotation(avatarData: userLiveLocation.avatarData, coordinate: userLiveLocation.coordinate) + } + } + + private func currentUserLocationAnnotation(from annotations: [UserLocationAnnotation]) -> UserLocationAnnotation? { + annotations.first { annotation in + return liveLocationSharingViewerService.isCurrentUserId(annotation.userId) + } + } + + private func getHighlightedAnnotation(from annotations: [UserLocationAnnotation]) -> UserLocationAnnotation? { + + if let userAnnotation = self.currentUserLocationAnnotation(from: annotations) { + return userAnnotation + } else { + return annotations.first + } + } + + private func listItemsViewData(from usersLiveLocation: [UserLiveLocation]) -> [LiveLocationListItemViewData] { + + var listItemsViewData: [LiveLocationListItemViewData] = [] + + let sortedUsersLiveLocation = usersLiveLocation.sorted { userLiveLocation1, userLiveLocation2 in + return userLiveLocation1.displayName > userLiveLocation2.displayName + } + + listItemsViewData = sortedUsersLiveLocation.map({ userLiveLocation in + return self.listItemViewData(from: userLiveLocation) + }) + + + let currentUserIndex = listItemsViewData.firstIndex { viewData in + return viewData.isCurrentUser + } + + // Move current user as first item + if let currentUserIndex = currentUserIndex { + + let currentUserViewData = listItemsViewData[currentUserIndex] + listItemsViewData.remove(at: currentUserIndex) + listItemsViewData.insert(currentUserViewData, at: 0) + } + + return listItemsViewData + } + + private func listItemViewData(from userLiveLocation: UserLiveLocation) -> LiveLocationListItemViewData { + + let isCurrentUser = self.liveLocationSharingViewerService.isCurrentUserId(userLiveLocation.userId) + + let expirationDate = userLiveLocation.timestamp + userLiveLocation.timeout + + return LiveLocationListItemViewData(userId: userLiveLocation.userId, isCurrentUser: isCurrentUser, avatarData: userLiveLocation.avatarData, displayName: userLiveLocation.displayName, expirationDate: expirationDate, lastUpdate: userLiveLocation.lastUpdate) + } + + private func update(with usersLiveLocation: [UserLiveLocation]) { + + let annotations: [UserLocationAnnotation] = self.userLocationAnnotations(from: usersLiveLocation) + + let highlightedAnnotation = self.getHighlightedAnnotation(from: annotations) + + let listViewItems = self.listItemsViewData(from: usersLiveLocation) + + self.state.annotations = annotations + self.state.highlightedAnnotation = highlightedAnnotation + self.state.listItemsViewData = listViewItems + } + + private func highlighAnnotation(with userId: String) { + let foundUserAnnotation = self.state.annotations.first { annotation in + annotation.userId == userId + } + + guard let foundUserAnnotation = foundUserAnnotation else { + return + } + + self.state.highlightedAnnotation = foundUserAnnotation + } +} diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/LiveLocationSharingViewerViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/LiveLocationSharingViewerViewModelProtocol.swift new file mode 100644 index 000000000..64f489745 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/LiveLocationSharingViewerViewModelProtocol.swift @@ -0,0 +1,24 @@ +// +// Copyright 2021 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 + +protocol LiveLocationSharingViewerViewModelProtocol { + + var completion: ((LiveLocationSharingViewerViewModelResult) -> Void)? { get set } + @available(iOS 14, *) + var context: LiveLocationSharingViewerViewModelType.Context { get } +} diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/MockLiveLocationSharingViewerScreenState.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/MockLiveLocationSharingViewerScreenState.swift new file mode 100644 index 000000000..10e198471 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/MockLiveLocationSharingViewerScreenState.swift @@ -0,0 +1,63 @@ +// +// Copyright 2021 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 SwiftUI + +/// Using an enum for the screen allows you define the different state cases with +/// the relevant associated data for each case. +@available(iOS 14.0, *) +enum MockLiveLocationSharingViewerScreenState: MockScreenState, CaseIterable { + // A case for each state you want to represent + // with specific, minimal associated data that will allow you + // mock that screen. + case currentUser + case multipleUsers + + /// The associated screen + var screenType: Any.Type { + LiveLocationSharingViewer.self + } + + /// A list of screen state definitions + static var allCases: [MockLiveLocationSharingViewerScreenState] { + return [.currentUser, .multipleUsers] + } + + /// Generate the view struct for the screen state. + var screenView: ([Any], AnyView) { + + let service: LiveLocationSharingViewerServiceProtocol + + switch self { + case .currentUser: + service = MockLiveLocationSharingViewerService() + case .multipleUsers: + service = MockLiveLocationSharingViewerService(generateRandomUsers: true) + } + + let mapStyleURL = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx")! + + let viewModel = LiveLocationSharingViewerViewModel(mapStyleURL: mapStyleURL, service: service) + + // can simulate service and viewModel actions here if needs be. + + return ( + [service, viewModel], + AnyView(LiveLocationSharingViewer(viewModel: viewModel.context)) + ) + } +} diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Test/UI/LiveLocationSharingViewerUITests.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Test/UI/LiveLocationSharingViewerUITests.swift new file mode 100644 index 000000000..3137d1381 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Test/UI/LiveLocationSharingViewerUITests.swift @@ -0,0 +1,26 @@ +// +// Copyright 2021 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 XCTest +import RiotSwiftUI + +@available(iOS 14.0, *) +class LiveLocationSharingViewerUITests: MockScreenTest { + + override class var screenType: MockScreenState.Type { + return MockLiveLocationSharingViewerScreenState.self + } +} diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Test/Unit/LiveLocationSharingViewerViewModelTests.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Test/Unit/LiveLocationSharingViewerViewModelTests.swift new file mode 100644 index 000000000..ec6f6a59b --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/Test/Unit/LiveLocationSharingViewerViewModelTests.swift @@ -0,0 +1,35 @@ +// +// Copyright 2021 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 XCTest +import Combine + +@testable import RiotSwiftUI + +@available(iOS 14.0, *) +class LiveLocationSharingViewerViewModelTests: XCTestCase { + + var service: MockLiveLocationSharingViewerService! + var viewModel: LiveLocationSharingViewerViewModelProtocol! + var context: LiveLocationSharingViewerViewModelType.Context! + var cancellables = Set() + + override func setUpWithError() throws { + service = MockLiveLocationSharingViewerService() + viewModel = LiveLocationSharingViewerViewModel(mapStyleURL: BuildSettings.tileServerMapStyleURL, service: service) + context = viewModel.context + } +} diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift new file mode 100644 index 000000000..389c0a0d7 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift @@ -0,0 +1,118 @@ +// +// Copyright 2021 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 +import DSBottomSheet + +@available(iOS 14.0, *) +struct LiveLocationSharingViewer: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + var isBottomSheetVisible = true + @State private var isBottomSheetExpanded = false + + // MARK: Public + + @ObservedObject var viewModel: LiveLocationSharingViewerViewModel.Context + + var body: some View { + ZStack(alignment: .bottom) { + LocationSharingMapView(tileServerMapURL: viewModel.viewState.mapStyleURL, + annotations: viewModel.viewState.annotations, + highlightedAnnotation: viewModel.viewState.highlightedAnnotation, + userAvatarData: nil, + showsUserLocation: false, + userLocation: Binding.constant(nil), + mapCenterCoordinate: Binding.constant(nil), + onCalloutTap: { annotation in + if let userLocationAnnotation = annotation as? UserLocationAnnotation { + viewModel.send(viewAction: .share(userLocationAnnotation)) + } + }, + errorSubject: viewModel.viewState.errorSubject) + MapCreditsView() + } + .bottomSheet(sheet, if: isBottomSheetVisible) + .alert(item: $viewModel.alertInfo) { info in + info.alert + } + .accentColor(theme.colors.accent) + .activityIndicator(show: viewModel.viewState.showLoadingIndicator) + } + + var userLocationList: some View { + ScrollView { + VStack(alignment: .leading, spacing: 14) { + ForEach(viewModel.viewState.listItemsViewData) { viewData in + LiveLocationListItem(viewData: viewData, onStopSharingAction: { + viewModel.send(viewAction: .stopSharing) + }, onBackgroundTap: { userId in + // Push bottom sheet down on item tap + isBottomSheetExpanded = false + viewModel.send(viewAction: .tapListItem(userId)) + }) + } + } + .padding() + } + } +} + +// MARK: - Bottom sheet +@available(iOS 14.0, *) +extension LiveLocationSharingViewer { + + var sheetStyle: BottomSheetStyle { + var bottomSheetStyle = BottomSheetStyle.standard + + bottomSheetStyle.snapRatio = 0.16 + + let backgroundColor = theme.colors.background + + let handleStyle = BottomSheetHandleStyle(backgroundColor: backgroundColor, dividerColor: backgroundColor) + bottomSheetStyle.handleStyle = handleStyle + + return bottomSheetStyle + } + + var sheet: some BottomSheetView { + BottomSheet( + isExpanded: $isBottomSheetExpanded, + minHeight: .points(150), + maxHeight: .available, + style: sheetStyle) { + userLocationList + } + } +} + +// MARK: - Previews + +@available(iOS 14.0, *) +struct LiveLocationSharingViewer_Previews: PreviewProvider { + static let stateRenderer = MockLiveLocationSharingViewerScreenState.stateRenderer + static var previews: some View { + Group { + stateRenderer.screenGroup().theme(.light).preferredColorScheme(.light) + stateRenderer.screenGroup().theme(.dark).preferredColorScheme(.dark) + } + } +} From d406fc3164cdf4eabd50856450308a0a3da1d269 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 5 Apr 2022 18:36:53 +0200 Subject: [PATCH 013/158] Update strings. --- Riot/Assets/en.lproj/Vector.strings | 7 +++++++ Riot/Generated/Strings.swift | 24 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 2cfb16062..851e50751 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2113,6 +2113,13 @@ Tap the + to start adding people."; "location_sharing_live_map_callout_title" = "Share location"; +"location_sharing_live_list_item_time_left" = "%@ left"; +"location_sharing_live_list_item_sharing_expired" = "Sharing expired"; +"location_sharing_live_list_item_last_update" = "Updated %@ ago"; +"location_sharing_live_list_item_last_update_invalid" = "Unknown last update"; +"location_sharing_live_list_item_current_user_display_name" = "You"; +"location_sharing_live_list_item_stop_sharing_action" = "Stop sharing"; + // MARK: - MatrixKit diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index a5695e27b..df6adf388 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2759,6 +2759,30 @@ public class VectorL10n: NSObject { public static var locationSharingInvalidAuthorizationSettings: String { return VectorL10n.tr("Vector", "location_sharing_invalid_authorization_settings") } + /// You + public static var locationSharingLiveListItemCurrentUserDisplayName: String { + return VectorL10n.tr("Vector", "location_sharing_live_list_item_current_user_display_name") + } + /// Updated %@ ago + public static func locationSharingLiveListItemLastUpdate(_ p1: String) -> String { + return VectorL10n.tr("Vector", "location_sharing_live_list_item_last_update", p1) + } + /// Unknown last update + public static var locationSharingLiveListItemLastUpdateInvalid: String { + return VectorL10n.tr("Vector", "location_sharing_live_list_item_last_update_invalid") + } + /// Sharing expired + public static var locationSharingLiveListItemSharingExpired: String { + return VectorL10n.tr("Vector", "location_sharing_live_list_item_sharing_expired") + } + /// Stop sharing + public static var locationSharingLiveListItemStopSharingAction: String { + return VectorL10n.tr("Vector", "location_sharing_live_list_item_stop_sharing_action") + } + /// %@ left + public static func locationSharingLiveListItemTimeLeft(_ p1: String) -> String { + return VectorL10n.tr("Vector", "location_sharing_live_list_item_time_left", p1) + } /// Share location public static var locationSharingLiveMapCalloutTitle: String { return VectorL10n.tr("Vector", "location_sharing_live_map_callout_title") From 809ab0be5b2ebbe9e9bafb3470f75e9d84e3fe5b Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 5 Apr 2022 18:37:01 +0200 Subject: [PATCH 014/158] Update share icon. --- .../Common/share_action_button.imageset/Contents.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Riot/Assets/Images.xcassets/Common/share_action_button.imageset/Contents.json b/Riot/Assets/Images.xcassets/Common/share_action_button.imageset/Contents.json index a6bb96098..1f52d4d2c 100644 --- a/Riot/Assets/Images.xcassets/Common/share_action_button.imageset/Contents.json +++ b/Riot/Assets/Images.xcassets/Common/share_action_button.imageset/Contents.json @@ -19,5 +19,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } From 46b423146d076b2ed3a1209ed68209da5bc202bd Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 5 Apr 2022 21:31:26 +0200 Subject: [PATCH 015/158] Update UserLocationAnnotation to handle annotation selection. --- .../Room/LocationSharing/LocationAnnotation.swift | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationAnnotation.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationAnnotation.swift index 545348f0e..3ddded983 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationAnnotation.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationAnnotation.swift @@ -17,26 +17,36 @@ import Foundation import Mapbox +/// Base class to handle a map annotation class LocationAnnotation: NSObject, MGLAnnotation { // MARK: - Properties + // Title property is needed to enable annotation selection and callout view showing + var title: String? + let coordinate: CLLocationCoordinate2D // MARK: - Setup init(coordinate: CLLocationCoordinate2D) { - self.coordinate = coordinate + super.init() } } +/// POI map annotation class PinLocationAnnotation: LocationAnnotation {} +/// User map annotation class UserLocationAnnotation: LocationAnnotation { // MARK: - Properties + var userId: String { + return avatarData.matrixItemId + } + let avatarData: AvatarInputProtocol // MARK: - Setup @@ -45,7 +55,8 @@ class UserLocationAnnotation: LocationAnnotation { coordinate: CLLocationCoordinate2D) { self.avatarData = avatarData - + super.init(coordinate: coordinate) + super.title = self.avatarData.displayName ?? self.userId } } From 6810efe441508a6dc4013d1505db9cf8fcd5431b Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 5 Apr 2022 21:32:40 +0200 Subject: [PATCH 016/158] Fix LocationAnnotatonView not touchable. --- .../View/LocationSharingMarkerView.swift | 1 - .../View/UserLocationAnnotatonView.swift | 49 ++++++++++++------- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMarkerView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMarkerView.swift index 3910b053f..3c36e7d50 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMarkerView.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMarkerView.swift @@ -40,7 +40,6 @@ struct LocationSharingMarkerView: View { markerImage .frame(width: 40, height: 40) } - .offset(x: 0, y: -23) } } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/UserLocationAnnotatonView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserLocationAnnotatonView.swift index 92395b483..11da1f5b7 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/View/UserLocationAnnotatonView.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserLocationAnnotatonView.swift @@ -21,30 +21,41 @@ import Mapbox @available(iOS 14, *) class LocationAnnotatonView: MGLUserLocationAnnotationView { + // MARK: - Constants + + private enum Constants { + static let defaultFrame = CGRect(x: 0, y: 0, width: 46, height: 46) + } + // MARK: Private @Environment(\.theme) private var theme: ThemeSwiftUI // MARK: - Setup - init(avatarData: AvatarInputProtocol) { - super.init(frame: .zero) - + override init(annotation: MGLAnnotation?, reuseIdentifier: String?) { + super.init(annotation: annotation, reuseIdentifier: + reuseIdentifier) + self.frame = Constants.defaultFrame + } + + convenience init(avatarData: AvatarInputProtocol) { + self.init(annotation: nil, reuseIdentifier: nil) self.addUserMarkerView(with: avatarData) } - init(userLocationAnnotation: UserLocationAnnotation) { + convenience init(userLocationAnnotation: UserLocationAnnotation) { // TODO: Use a reuseIdentifier - super.init(annotation: userLocationAnnotation, reuseIdentifier: nil) + self.init(annotation: userLocationAnnotation, reuseIdentifier: nil) self.addUserMarkerView(with: userLocationAnnotation.avatarData) - } - init(pinLocationAnnotation: PinLocationAnnotation) { + convenience init(pinLocationAnnotation: PinLocationAnnotation) { + // TODO: Use a reuseIdentifier - super.init(annotation: pinLocationAnnotation, reuseIdentifier: nil) + self.init(annotation: pinLocationAnnotation, reuseIdentifier: nil) self.addPinMarkerView() } @@ -57,32 +68,34 @@ class LocationAnnotatonView: MGLUserLocationAnnotationView { private func addUserMarkerView(with avatarData: AvatarInputProtocol) { - guard let avatarImageView = UIHostingController(rootView: LocationSharingMarkerView(backgroundColor: theme.userColor(for: avatarData.matrixItemId)) { + guard let avatarMarkerView = UIHostingController(rootView: LocationSharingMarkerView(backgroundColor: theme.userColor(for: avatarData.matrixItemId)) { AvatarImage(avatarData: avatarData, size: .medium) .border() }).view else { return } - addMarkerView(with: avatarImageView) + + addMarkerView(avatarMarkerView) } private func addPinMarkerView() { - guard let pinImageView = UIHostingController(rootView: LocationSharingMarkerView(backgroundColor: theme.colors.accent) { + guard let pinMarkerView = UIHostingController(rootView: LocationSharingMarkerView(backgroundColor: theme.colors.accent) { Image(uiImage: Asset.Images.locationPinIcon.image) .resizable() .shapedBorder(color: theme.colors.accent, borderWidth: 3, shape: Circle()) }).view else { return } - addMarkerView(with: pinImageView) + + addMarkerView(pinMarkerView) } - private func addMarkerView(with imageView: UIView) { - addSubview(imageView) + private func addMarkerView(_ markerView: UIView) { - addConstraints([topAnchor.constraint(equalTo: imageView.topAnchor), - leadingAnchor.constraint(equalTo: imageView.leadingAnchor), - bottomAnchor.constraint(equalTo: imageView.bottomAnchor), - trailingAnchor.constraint(equalTo: imageView.trailingAnchor)]) + markerView.backgroundColor = .clear + + addSubview(markerView) + + markerView.frame = self.bounds } } From 5f1bacadc074f371af2f4681d26c78b29c5fa178 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 5 Apr 2022 21:33:05 +0200 Subject: [PATCH 017/158] Update UserAnnotationCalloutView center. --- .../Room/LocationSharing/View/UserAnnotationCalloutView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutView.swift index 50721c6fa..64edcfd30 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutView.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutView.swift @@ -46,7 +46,7 @@ class UserAnnotationCalloutView: UIView, MGLCalloutView, Themable { override var center: CGPoint { set { var newCenter = newValue - newCenter.y -= bounds.maxY + Constants.bottomMargin + newCenter.y -= bounds.midY + Constants.bottomMargin super.center = newCenter } get { From a9073521ff1b360b80d7cad92051f01b09c33091 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Tue, 5 Apr 2022 21:34:22 +0200 Subject: [PATCH 018/158] LiveLocationSharingViewer: Enable map callout display. --- .../View/LiveLocationSharingViewer.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift index 389c0a0d7..1b3562671 100644 --- a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift @@ -40,6 +40,7 @@ struct LiveLocationSharingViewer: View { highlightedAnnotation: viewModel.viewState.highlightedAnnotation, userAvatarData: nil, showsUserLocation: false, + userAnnotationCanShowCallout: true, userLocation: Binding.constant(nil), mapCenterCoordinate: Binding.constant(nil), onCalloutTap: { annotation in From e69036ac8cc261f3ed4dd3aac92b44b0a563dce1 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Wed, 6 Apr 2022 09:19:06 +0200 Subject: [PATCH 019/158] Add changes --- changelog.d/5273.wip | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5273.wip diff --git a/changelog.d/5273.wip b/changelog.d/5273.wip new file mode 100644 index 000000000..f5a7245dc --- /dev/null +++ b/changelog.d/5273.wip @@ -0,0 +1 @@ +Location sharing: Add live location viewer screen. From b0264cf304279d9cc4b43f21e417c98884d8df6b Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Wed, 6 Apr 2022 09:19:22 +0200 Subject: [PATCH 020/158] Expose UIView extension to SwiftUI target. --- RiotSwiftUI/target.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/RiotSwiftUI/target.yml b/RiotSwiftUI/target.yml index 58f3e74f3..f2feb1940 100644 --- a/RiotSwiftUI/target.yml +++ b/RiotSwiftUI/target.yml @@ -52,6 +52,7 @@ targets: - path: ../Riot/Categories/Character.swift - path: ../Riot/Categories/UIColor.swift - path: ../Riot/Categories/UISearchBar.swift + - path: ../Riot/Categories/UIView.swift - path: ../Riot/Assets/en.lproj/Vector.strings - path: ../Riot/Modules/Analytics/AnalyticsScreen.swift - path: ../Riot/Assets/en.lproj/Untranslated.strings From c04022a76c4bbda0b7a09500e1e7b5faa2e50b53 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Wed, 6 Apr 2022 09:41:33 +0200 Subject: [PATCH 021/158] LiveLocationSharingViewer: Add navigation title. --- Riot/Assets/en.lproj/Vector.strings | 2 +- Riot/Generated/Strings.swift | 4 ++++ .../View/LiveLocationSharingViewer.swift | 3 ++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 851e50751..91e22ad06 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2112,7 +2112,7 @@ Tap the + to start adding people."; "location_sharing_pin_drop_share_title" = "Send this location"; "location_sharing_live_map_callout_title" = "Share location"; - +"location_sharing_live_viewer_title" = "Location"; "location_sharing_live_list_item_time_left" = "%@ left"; "location_sharing_live_list_item_sharing_expired" = "Sharing expired"; "location_sharing_live_list_item_last_update" = "Updated %@ ago"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index df6adf388..9969e67de 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2791,6 +2791,10 @@ public class VectorL10n: NSObject { public static var locationSharingLiveShareTitle: String { return VectorL10n.tr("Vector", "location_sharing_live_share_title") } + /// Location + public static var locationSharingLiveViewerTitle: String { + return VectorL10n.tr("Vector", "location_sharing_live_viewer_title") + } /// %@ could not load the map. Please try again later. public static func locationSharingLoadingMapErrorTitle(_ p1: String) -> String { return VectorL10n.tr("Vector", "location_sharing_loading_map_error_title", p1) diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift index 1b3562671..ca638ab55 100644 --- a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift @@ -51,11 +51,12 @@ struct LiveLocationSharingViewer: View { errorSubject: viewModel.viewState.errorSubject) MapCreditsView() } + .navigationTitle(VectorL10n.locationSharingLiveViewerTitle) + .accentColor(theme.colors.accent) .bottomSheet(sheet, if: isBottomSheetVisible) .alert(item: $viewModel.alertInfo) { info in info.alert } - .accentColor(theme.colors.accent) .activityIndicator(show: viewModel.viewState.showLoadingIndicator) } From 6516e76ea41204f61edc81051e4ead5502520cf7 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Wed, 6 Apr 2022 09:55:45 +0200 Subject: [PATCH 022/158] LiveLocationSharingViewer: Fix credits display. --- .../View/LiveLocationSharingViewer.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift index ca638ab55..790fbc580 100644 --- a/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift +++ b/RiotSwiftUI/Modules/Room/LiveLocationSharingViewer/View/LiveLocationSharingViewer.swift @@ -49,7 +49,11 @@ struct LiveLocationSharingViewer: View { } }, errorSubject: viewModel.viewState.errorSubject) - MapCreditsView() + VStack(alignment: .center) { + Spacer() + MapCreditsView() + .offset(y: -130) + } } .navigationTitle(VectorL10n.locationSharingLiveViewerTitle) .accentColor(theme.colors.accent) From 63b36feda73b49fed6b85a03807fd7701088cb93 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Wed, 6 Apr 2022 10:12:03 +0200 Subject: [PATCH 023/158] Fix changes --- changelog.d/{5273.wip => 5723.wip} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{5273.wip => 5723.wip} (100%) diff --git a/changelog.d/5273.wip b/changelog.d/5723.wip similarity index 100% rename from changelog.d/5273.wip rename to changelog.d/5723.wip From 1d8611b397a63d58512c9a87c5c04c252e19fbde Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Wed, 6 Apr 2022 10:19:43 +0200 Subject: [PATCH 024/158] UserAnnotationCalloutContentView: Update layout. --- .../View/UserAnnotationCalloutContentView.xib | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutContentView.xib b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutContentView.xib index 8b438cac2..45c9be07f 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutContentView.xib +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/UserAnnotationCalloutContentView.xib @@ -10,7 +10,7 @@ - + @@ -19,7 +19,7 @@ + diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Location/LocationPlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Location/LocationPlainCell.swift index 4b86e4209..5628f4ef4 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Location/LocationPlainCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Location/LocationPlainCell.swift @@ -15,6 +15,7 @@ // import Foundation +import MatrixSDK class LocationPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable, RoomCellReadMarkerDisplayable { @@ -25,14 +26,25 @@ class LocationPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable, Room guard #available(iOS 14.0, *), let bubbleData = cellData as? RoomBubbleCellData, - let event = bubbleData.events.last, - event.eventType == __MXEventType.roomMessage, - let locationContent = event.location + let event = bubbleData.events.last else { return } locationView.update(theme: ThemeService.shared().theme) + + if event.eventType == __MXEventType.roomMessage { + renderStaticLocation(event) + } else if event.eventType == __MXEventType.beaconInfo { + renderLiveLocation(event) + } + } + + private func renderStaticLocation(_ event: MXEvent) { + guard let locationContent = event.location else { + return + } + locationView.locationDescription = locationContent.locationDescription let location = CLLocationCoordinate2D(latitude: locationContent.latitude, longitude: locationContent.longitude) @@ -52,6 +64,12 @@ class LocationPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable, Room } } + private func renderLiveLocation(_ event: MXEvent) { + guard let locationContent = event.content else { + return + } + } + override func setupViews() { super.setupViews() From d23ac4e7c52189e95bde0951ed2b878387aa563f Mon Sep 17 00:00:00 2001 From: Doug Date: Wed, 20 Apr 2022 17:40:44 +0100 Subject: [PATCH 058/158] Fix UI Tests and run on PRs - Add missing screen states. - Detect the bottom of the screen list and stop scrolling if screen state wasn't found. - Remove unimplemented tests to speed up the run. - Remove failed button checks in MatrixItemChooserUITests --- .github/workflows/ci-ui-tests.yml | 57 +++++++++++++++++++ .github/workflows/release-alpha.yml | 4 +- Riot/Generated/Strings.swift | 21 ++++--- .../Modules/Common/Mock/MockAppScreens.swift | 4 ++ .../Modules/Common/Mock/ScreenList.swift | 15 +++-- .../Common/Test/UI/XCUIApplication+Riot.swift | 3 +- .../UI/RoomAccessTypeChooserUITests.swift | 14 +---- .../Test/UI/RoomUpgradeUITests.swift | 14 +---- .../UI/StaticLocationViewingUITests.swift | 34 ++++++----- .../View/StaticLocationView.swift | 2 +- .../MockMatrixItemChooserScreenState.swift | 4 +- .../Test/UI/MatrixItemChooserUITests.swift | 3 - .../Test/UI/SpaceSettingsUITests.swift | 14 +---- RiotSwiftUI/targetUITests.yml | 1 + .../Strings/flat-swift4-vector.stencil | 23 ++++---- changelog.d/6050.build | 1 + 16 files changed, 131 insertions(+), 83 deletions(-) create mode 100644 .github/workflows/ci-ui-tests.yml create mode 100644 changelog.d/6050.build diff --git a/.github/workflows/ci-ui-tests.yml b/.github/workflows/ci-ui-tests.yml new file mode 100644 index 000000000..eb2dde14d --- /dev/null +++ b/.github/workflows/ci-ui-tests.yml @@ -0,0 +1,57 @@ +name: UI Tests CI + +on: + # Triggers the workflow on any pull request + pull_request: + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +env: + # Make the git branch for a PR available to our Fastfile + MX_GIT_BRANCH: ${{ github.event.pull_request.head.ref }} + +jobs: + tests: + name: UI Tests + runs-on: macos-11 + + concurrency: + # Only allow a single run of this workflow on each branch, automatically cancelling older runs. + group: ui-tests-${{ github.head_ref }} + cancel-in-progress: true + + steps: + - uses: actions/checkout@v2 + + # Common cache + # Note: GH actions do not support yaml anchor yet. We need to duplicate this for every job + - uses: actions/cache@v2 + with: + path: Pods + key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} + restore-keys: | + ${{ runner.os }}-pods- + - uses: actions/cache@v2 + with: + path: vendor/bundle + key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: | + ${{ runner.os }}-gems- + + # Make sure we use the latest version of MatrixSDK + - name: Reset MatrixSDK pod + run: rm -rf Pods/MatrixSDK + + # Common setup + # Note: GH actions do not support yaml anchor yet. We need to duplicate this for every job + - name: Bundle install + run: | + bundle config path vendor/bundle + bundle install --jobs 4 --retry 3 + - name: Use right MatrixSDK versions + run: bundle exec fastlane point_dependencies_to_related_branches + + # Main step + - name: Unit tests + run: bundle exec fastlane uitest diff --git a/.github/workflows/release-alpha.yml b/.github/workflows/release-alpha.yml index c952f6a4a..acdfa1b1d 100644 --- a/.github/workflows/release-alpha.yml +++ b/.github/workflows/release-alpha.yml @@ -25,7 +25,7 @@ jobs: if: "${{ env.P12_KEY != '' || env.P12_PASSWORD_KEY != '' }}" run: echo "::set-output name=defined::true" build: - # Run job if secrets are avilable (not avaiable for forks). + # Run job if secrets are available (not available for forks). needs: [check-secret] if: needs.check-secret.outputs.out-key == 'true' name: Release @@ -84,7 +84,7 @@ jobs: - name: Build Ad-hoc release and send it to Diawi run: bundle exec fastlane alpha env: - # Automaticaly bypass 2FA upgrade if possible on Apple account. + # Automatically bypass 2FA upgrade if possible on Apple account. SPACESHIP_SKIP_2FA_UPGRADE: true APPLE_ID: ${{ secrets.FASTLANE_USER }} FASTLANE_USER: ${{ secrets.FASTLANE_USER }} diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 012642ad4..ba97510e9 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -8106,15 +8106,18 @@ public class VectorL10n: NSObject { extension VectorL10n { static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String { - let format = NSLocalizedString(key, tableName: table, bundle: Bundle.app, comment: "") - let locale: Locale - if let providedLocale = LocaleProvider.locale { - locale = providedLocale - } else { - locale = Locale.current - } - - return String(format: format, locale: locale, arguments: args) + let format = NSLocalizedString(key, tableName: table, bundle: bundle, comment: "") + let locale = LocaleProvider.locale ?? Locale.current + return String(format: format, locale: locale, arguments: args) + } + /// The bundle to load strings from. This will be the app's bundle unless running + /// the UI tests target, in which case the strings are contained in the tests bundle. + static let bundle: Bundle = { + if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil { + // The tests bundle is embedded inside a runner. Find the bundle for VectorL10n. + return Bundle(for: VectorL10n.self) } + return Bundle.app + }() } diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift index cae73c224..8fc72fc62 100644 --- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift +++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift @@ -26,11 +26,15 @@ enum MockAppScreens { MockOnboardingCongratulationsScreenState.self, MockOnboardingUseCaseSelectionScreenState.self, MockOnboardingSplashScreenScreenState.self, + MockStaticLocationViewingScreenState.self, MockLocationSharingScreenState.self, MockAnalyticsPromptScreenState.self, MockUserSuggestionScreenState.self, MockPollEditFormScreenState.self, MockSpaceCreationEmailInvitesScreenState.self, + MockSpaceSettingsScreenState.self, + MockRoomAccessTypeChooserScreenState.self, + MockRoomUpgradeScreenState.self, MockMatrixItemChooserScreenState.self, MockSpaceCreationMenuScreenState.self, MockSpaceCreationRoomsScreenState.self, diff --git a/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift b/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift index 5e29d179b..faf39e0e4 100644 --- a/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift +++ b/RiotSwiftUI/Modules/Common/Mock/ScreenList.swift @@ -30,12 +30,19 @@ struct ScreenList: View { var body: some View { NavigationView { List { - ForEach(0.. MockScreenTest { - return RoomAccessTypeChooserUITests(selector: #selector(verifyRoomAccessTypeChooserScreen)) - } - - func verifyRoomAccessTypeChooserScreen() throws { - guard let screenState = screenState as? MockRoomAccessTypeChooserScreenState else { fatalError("no screen") } - } - + // Tests to be implemented. } diff --git a/RiotSwiftUI/Modules/Room/RoomUpgrade/Test/UI/RoomUpgradeUITests.swift b/RiotSwiftUI/Modules/Room/RoomUpgrade/Test/UI/RoomUpgradeUITests.swift index c7463ab27..e0e61b67e 100644 --- a/RiotSwiftUI/Modules/Room/RoomUpgrade/Test/UI/RoomUpgradeUITests.swift +++ b/RiotSwiftUI/Modules/Room/RoomUpgrade/Test/UI/RoomUpgradeUITests.swift @@ -19,17 +19,5 @@ import RiotSwiftUI @available(iOS 14.0, *) class RoomUpgradeUITests: MockScreenTest { - - override class var screenType: MockScreenState.Type { - return MockRoomUpgradeScreenState.self - } - - override class func createTest() -> MockScreenTest { - return RoomUpgradeUITests(selector: #selector(verifyRoomUpgradeScreen)) - } - - func verifyRoomUpgradeScreen() throws { - guard let screenState = screenState as? MockRoomUpgradeScreenState else { fatalError("no screen") } - } - + // Tests to be implemented. } diff --git a/RiotSwiftUI/Modules/Room/StaticLocationSharingViewer/Test/UI/StaticLocationViewingUITests.swift b/RiotSwiftUI/Modules/Room/StaticLocationSharingViewer/Test/UI/StaticLocationViewingUITests.swift index 8e73be761..f9c98e0be 100644 --- a/RiotSwiftUI/Modules/Room/StaticLocationSharingViewer/Test/UI/StaticLocationViewingUITests.swift +++ b/RiotSwiftUI/Modules/Room/StaticLocationSharingViewer/Test/UI/StaticLocationViewingUITests.swift @@ -19,21 +19,29 @@ import RiotSwiftUI @available(iOS 14.0, *) class StaticLocationViewingUITests: MockScreenTest { - - private var app: XCUIApplication! - override func setUp() { - continueAfterFailure = false - - app = XCUIApplication() - app.launch() + override class var screenType: MockScreenState.Type { + return MockStaticLocationViewingScreenState.self + } + + override class func createTest() -> MockScreenTest { + return StaticLocationViewingUITests(selector: #selector(verifyStaticLocationViewingScreen)) + } + + func verifyStaticLocationViewingScreen() { + guard let screenState = screenState as? MockStaticLocationViewingScreenState else { fatalError("no screen") } + + switch screenState { + case .showUserLocation: + verifyInitialExistingLocation() + case .showPinLocation: + verifyInitialExistingLocation() + } } - func testInitialExistingLocation() { - goToScreenWithIdentifier(MockStaticLocationViewingScreenState.showUserLocation.title) - - XCTAssertTrue(app.buttons["Cancel"].exists) - XCTAssertTrue(app.buttons["StaticLocationView.shareButton"].exists) - XCTAssertTrue(app.otherElements["Map"].exists) + func verifyInitialExistingLocation() { + XCTAssertTrue(app.buttons["Cancel"].exists, "The cancel button should exist.") + XCTAssertTrue(app.buttons["shareButton"].exists, "The share button should exist.") + XCTAssertTrue(app.otherElements["Map"].exists, "The map view should exist.") } } diff --git a/RiotSwiftUI/Modules/Room/StaticLocationSharingViewer/View/StaticLocationView.swift b/RiotSwiftUI/Modules/Room/StaticLocationSharingViewer/View/StaticLocationView.swift index 1ba73aac0..a217d3afc 100644 --- a/RiotSwiftUI/Modules/Room/StaticLocationSharingViewer/View/StaticLocationView.swift +++ b/RiotSwiftUI/Modules/Room/StaticLocationSharingViewer/View/StaticLocationView.swift @@ -61,9 +61,9 @@ struct StaticLocationView: View { viewModel.send(viewAction: .share) } label: { Image(uiImage: Asset.Images.locationShareIcon.image) - .accessibilityIdentifier("LocationSharingView.shareButton") } .disabled(!viewModel.viewState.shareButtonEnabled) + .accessibilityIdentifier("shareButton") } } .navigationBarTitleDisplayMode(.inline) diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MockMatrixItemChooserScreenState.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MockMatrixItemChooserScreenState.swift index a8f1d729b..29f07f534 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MockMatrixItemChooserScreenState.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/MockMatrixItemChooserScreenState.swift @@ -44,7 +44,9 @@ enum MockMatrixItemChooserScreenState: MockScreenState, CaseIterable { case .selectedItems: service = MockMatrixItemChooserService(type: .room, sections: MockMatrixItemChooserService.mockSections, selectedItemIndexPaths: [IndexPath(row: 0, section: 0), IndexPath(row: 2, section: 0), IndexPath(row: 1, section: 1)]) } - let viewModel = MatrixItemChooserViewModel.makeMatrixItemChooserViewModel(matrixItemChooserService: service, title: "Some title", detail: "Detail text describing the current screen") + let viewModel = MatrixItemChooserViewModel.makeMatrixItemChooserViewModel(matrixItemChooserService: service, + title: VectorL10n.spacesCreationAddRoomsTitle, + detail: VectorL10n.spacesCreationAddRoomsMessage) // can simulate service and viewModel actions here if needs be. diff --git a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/UI/MatrixItemChooserUITests.swift b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/UI/MatrixItemChooserUITests.swift index e292151b9..c5b9c9a96 100644 --- a/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/UI/MatrixItemChooserUITests.swift +++ b/RiotSwiftUI/Modules/Spaces/MatrixItemChooser/Test/UI/MatrixItemChooserUITests.swift @@ -45,21 +45,18 @@ class MatrixItemChooserUITests: MockScreenTest { XCTAssertEqual(app.staticTexts["messageText"].label, VectorL10n.spacesCreationAddRoomsMessage) XCTAssertEqual(app.staticTexts["emptyListMessage"].exists, true) XCTAssertEqual(app.staticTexts["emptyListMessage"].label, VectorL10n.spacesNoResultFoundTitle) - XCTAssertEqual(app.buttons["doneButton"].label, VectorL10n.skip) } func verifyPopulatedScreen() { XCTAssertEqual(app.staticTexts["titleText"].label, VectorL10n.spacesCreationAddRoomsTitle) XCTAssertEqual(app.staticTexts["messageText"].label, VectorL10n.spacesCreationAddRoomsMessage) XCTAssertEqual(app.staticTexts["emptyListMessage"].exists, false) - XCTAssertEqual(app.buttons["doneButton"].label, VectorL10n.skip) } func verifyPopulatedWithSelectionScreen() { XCTAssertEqual(app.staticTexts["titleText"].label, VectorL10n.spacesCreationAddRoomsTitle) XCTAssertEqual(app.staticTexts["messageText"].label, VectorL10n.spacesCreationAddRoomsMessage) XCTAssertEqual(app.staticTexts["emptyListMessage"].exists, false) - XCTAssertEqual(app.buttons["doneButton"].label, VectorL10n.next) } } diff --git a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Test/UI/SpaceSettingsUITests.swift b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Test/UI/SpaceSettingsUITests.swift index 2e47c2f76..1449bd93d 100644 --- a/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Test/UI/SpaceSettingsUITests.swift +++ b/RiotSwiftUI/Modules/Spaces/SpaceSettings/SpaceSettings/Test/UI/SpaceSettingsUITests.swift @@ -19,17 +19,5 @@ import RiotSwiftUI @available(iOS 14.0, *) class SpaceSettingsUITests: MockScreenTest { - - override class var screenType: MockScreenState.Type { - return MockSpaceSettingsScreenState.self - } - - override class func createTest() -> MockScreenTest { - return SpaceSettingsUITests(selector: #selector(verifySpaceSettingsScreen)) - } - - func verifySpaceSettingsScreen() throws { - guard let screenState = screenState as? MockSpaceSettingsScreenState else { fatalError("no screen") } - } - + // Tests to be implemented. } diff --git a/RiotSwiftUI/targetUITests.yml b/RiotSwiftUI/targetUITests.yml index 324fcb0d0..86e38b532 100644 --- a/RiotSwiftUI/targetUITests.yml +++ b/RiotSwiftUI/targetUITests.yml @@ -39,6 +39,7 @@ targets: PRODUCT_BUNDLE_IDENTIFIER: org.matrix.RiotSwiftUITests$(rfc1034identifier) SWIFT_OBJC_BRIDGING_HEADER: $(SRCROOT)/RiotSwiftUI/RiotSwiftUI-Bridging-Header.h SWIFT_OBJC_INTERFACE_HEADER_NAME: GeneratedInterface-Swift.h + GENERATE_INFOPLIST_FILE: YES sources: # Source included/excluded here here are similar to RiotSwiftUI as we # need access to ScreenStates diff --git a/Tools/SwiftGen/Templates/Strings/flat-swift4-vector.stencil b/Tools/SwiftGen/Templates/Strings/flat-swift4-vector.stencil index 3e3e6f594..133587548 100644 --- a/Tools/SwiftGen/Templates/Strings/flat-swift4-vector.stencil +++ b/Tools/SwiftGen/Templates/Strings/flat-swift4-vector.stencil @@ -64,17 +64,20 @@ import Foundation extension {{className}} { static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String { - let format = NSLocalizedString(key, tableName: table, bundle: Bundle.app, comment: "") - let locale: Locale - - if let providedLocale = LocaleProvider.locale { - locale = providedLocale - } else { - locale = Locale.current - } - - return String(format: format, locale: locale, arguments: args) + let format = NSLocalizedString(key, tableName: table, bundle: bundle, comment: "") + let locale = LocaleProvider.locale ?? Locale.current + + return String(format: format, locale: locale, arguments: args) + } + /// The bundle to load strings from. This will be the app's bundle unless running + /// the UI tests target, in which case the strings are contained in the tests bundle. + static let bundle: Bundle = { + if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil { + // The tests bundle is embedded inside a runner. Find the bundle for VectorL10n. + return Bundle(for: VectorL10n.self) } + return Bundle.app + }() } {% else %} diff --git a/changelog.d/6050.build b/changelog.d/6050.build new file mode 100644 index 000000000..68d4e07c8 --- /dev/null +++ b/changelog.d/6050.build @@ -0,0 +1 @@ +UI Tests: Fix broken tests and add a check on PRs. From d9d241c58cfc5f0baa29045ce93737d95dadce63 Mon Sep 17 00:00:00 2001 From: MaximeE Date: Thu, 21 Apr 2022 17:29:32 +0200 Subject: [PATCH 059/158] 6029: finalise configuration of live location cell --- .../location_live_icon.imageset/Contents.json | 3 - Riot/Assets/en.lproj/Vector.strings | 2 + Riot/Generated/Strings.swift | 8 ++ .../Room/CellData/RoomBubbleCellData.m | 6 - .../Location/RoomTimelineLocationView.swift | 39 +++++- .../Location/RoomTimelineLocationView.xib | 121 ++++++++---------- .../Cells/Location/LocationPlainCell.swift | 56 +++++++- changelog.d/6029.change | 1 + 8 files changed, 148 insertions(+), 88 deletions(-) create mode 100644 changelog.d/6029.change diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_live_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Location/location_live_icon.imageset/Contents.json index a7fdd32cc..0a20c899a 100644 --- a/Riot/Assets/Images.xcassets/Room/Location/location_live_icon.imageset/Contents.json +++ b/Riot/Assets/Images.xcassets/Room/Location/location_live_icon.imageset/Contents.json @@ -19,8 +19,5 @@ "info" : { "author" : "xcode", "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" } } diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 8a71db11e..83f855569 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2143,6 +2143,8 @@ Tap the + to start adding people."; "location_sharing_static_share_title" = "Send my current location"; "location_sharing_pin_drop_share_title" = "Send this location"; "location_sharing_live_map_callout_title" = "Share location"; +"location_sharing_live_timer_outgoing" = "%@ left"; +"location_sharing_live_timer_incoming" = "Live until %@"; // MARK: - MatrixKit diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 66236b6e3..13aecc0c7 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2771,6 +2771,14 @@ public class VectorL10n: NSObject { public static var locationSharingLiveShareTitle: String { return VectorL10n.tr("Vector", "location_sharing_live_share_title") } + /// Live until %@ + public static func locationSharingLiveTimerIncoming(_ p1: String) -> String { + return VectorL10n.tr("Vector", "location_sharing_live_timer_incoming", p1) + } + /// %@ left + public static func locationSharingLiveTimerOutgoing(_ p1: String) -> String { + return VectorL10n.tr("Vector", "location_sharing_live_timer_outgoing", p1) + } /// %@ could not load the map. Please try again later. public static func locationSharingLoadingMapErrorTitle(_ p1: String) -> String { return VectorL10n.tr("Vector", "location_sharing_loading_map_error_title", p1) diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index 6916cbeec..9d0603f6d 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -187,12 +187,6 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat break; } - case MXEventTypeBeaconInfo: - { - self.tag = RoomBubbleCellDataTagLocation; - self.collapsable = NO; - self.collapsed = NO; - } default: break; } diff --git a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift index 6573453fa..4eb372b2d 100644 --- a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift +++ b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift @@ -17,11 +17,28 @@ import UIKit import Reusable import Mapbox +import SwiftUI struct LiveLocationParameter { + let bannerImage: UIImage + let bannerTitle: String + let timer: String? + let shouldShowStopButton: Bool let isLive: Bool - let endTime: Int - let isUser: Bool +} + +enum LiveLocationState { + case incomingLive(String?) + case outgoingLive(String?) + + func values() -> LiveLocationParameter { + switch self { + case .incomingLive(let timerString): + return LiveLocationParameter(bannerImage: Asset.Images.locationLiveIcon.image, bannerTitle: VectorL10n.liveLocationSharingBannerTitle, timer: timerString, shouldShowStopButton: false, isLive: true) + case .outgoingLive(let timerString): + return LiveLocationParameter(bannerImage: Asset.Images.locationLiveIcon.image, bannerTitle: VectorL10n.liveLocationSharingBannerTitle, timer: timerString, shouldShowStopButton: true, isLive: true) + } + } } class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegate { @@ -48,7 +65,8 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat @IBOutlet private var liveLocationImageView: UIImageView! @IBOutlet private var liveLocationStatusLabel: UILabel! @IBOutlet private var liveLocationTimerLabel: UILabel! - @IBOutlet private var stopSharingContainerView: UIView! + @IBOutlet private var stopSharingButton: UIButton! + private var mapView: MGLMapView! @@ -91,7 +109,7 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat public func displayLocation(_ location: CLLocationCoordinate2D, userAvatarData: AvatarViewData? = nil, mapStyleURL: URL, - liveLocationParameter: LiveLocationParameter? = nil) { + liveLocationState: LiveLocationState? = nil) { mapView.styleURL = mapStyleURL @@ -113,14 +131,21 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat mapView.addAnnotation(pointAnnotation) // Configure live location banner - guard let liveLocationParameter = liveLocationParameter else { + guard let liveLocationParameters = liveLocationState?.values() else { liveLocationContainerView.isHidden = true return } liveLocationContainerView.isHidden = false - stopSharingContainerView.isHidden = !liveLocationParameter.isLive - liveLocationTimerLabel.isHidden = !liveLocationParameter.isLive + liveLocationImageView.image = liveLocationParameters.bannerImage + liveLocationStatusLabel.text = liveLocationParameters.bannerTitle + if let timerString = liveLocationParameters.timer { + liveLocationTimerLabel.isHidden = false + liveLocationTimerLabel.text = timerString + } else { + liveLocationTimerLabel.isHidden = true + } + stopSharingButton.isHidden = !liveLocationParameters.shouldShowStopButton } // MARK: - Themable diff --git a/Riot/Modules/Room/Location/RoomTimelineLocationView.xib b/Riot/Modules/Room/Location/RoomTimelineLocationView.xib index da3e09108..c6cdf2ed3 100644 --- a/Riot/Modules/Room/Location/RoomTimelineLocationView.xib +++ b/Riot/Modules/Room/Location/RoomTimelineLocationView.xib @@ -15,16 +15,16 @@ - - + + - - + - - + + + + + + + + + - - + - - - - - - - - - - - - - - - - - - - - + + + + + - + - - - - + + + + + + + + + + + diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Location/LocationPlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Location/LocationPlainCell.swift index 5628f4ef4..4d47dd775 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Location/LocationPlainCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Location/LocationPlainCell.swift @@ -33,11 +33,9 @@ class LocationPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable, Room locationView.update(theme: ThemeService.shared().theme) - if event.eventType == __MXEventType.roomMessage { - renderStaticLocation(event) - } else if event.eventType == __MXEventType.beaconInfo { - renderLiveLocation(event) - } + // Comment this line and uncomment next one to test UI of live location tile + renderStaticLocation(event) +// renderLiveLocation(event) } private func renderStaticLocation(_ event: MXEvent) { @@ -65,11 +63,57 @@ class LocationPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable, Room } private func renderLiveLocation(_ event: MXEvent) { - guard let locationContent = event.content else { + // TODO: - Render live location cell when live location event is handled + + // This code is only for testing live location cell + // Will be completed when the live location event is handled + + guard let locationContent = event.location else { return } + + locationView.locationDescription = locationContent.locationDescription + + let location = CLLocationCoordinate2D(latitude: locationContent.latitude, longitude: locationContent.longitude) + + let mapStyleURL = bubbleData.mxSession.vc_homeserverConfiguration().tileServer.mapStyleURL + + let avatarViewData = AvatarViewData(matrixItemId: bubbleData.senderId, + displayName: bubbleData.senderDisplayName, + avatarUrl: bubbleData.senderAvatarUrl, + mediaManager: bubbleData.mxSession.mediaManager, + fallbackImage: .matrixItem(bubbleData.senderId, bubbleData.senderDisplayName)) + let futurDateTimeInterval = Date(timeIntervalSinceNow: 3734).timeIntervalSince1970 + locationView.displayLocation(location, userAvatarData: avatarViewData, mapStyleURL: mapStyleURL, liveLocationState: .outgoingLive(generateTimerString(for: futurDateTimeInterval, isIncomingLocation: false))) } + private func generateTimerString(for timestamp: Double, + isIncomingLocation: Bool) -> String? { + let timerString: String? + if isIncomingLocation { + timerString = VectorL10n.locationSharingLiveTimerIncoming(incomingTimerFormatter.string(from: Date(timeIntervalSince1970: timestamp))) + } else if let outgoingTimer = outgoingTimerFormatter.string(from: Date(timeIntervalSince1970: timestamp).timeIntervalSinceNow) { + timerString = VectorL10n.locationSharingLiveTimerOutgoing(outgoingTimer) + } else { + timerString = nil + } + return timerString + } + + private lazy var incomingTimerFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "HH:mm" + return dateFormatter + }() + + private lazy var outgoingTimerFormatter: DateComponentsFormatter = { + let formatter = DateComponentsFormatter() + formatter.zeroFormattingBehavior = .dropAll + formatter.allowedUnits = [.hour, .minute, .second] + formatter.unitsStyle = .brief + return formatter + }() + override func setupViews() { super.setupViews() diff --git a/changelog.d/6029.change b/changelog.d/6029.change new file mode 100644 index 000000000..9a829ea07 --- /dev/null +++ b/changelog.d/6029.change @@ -0,0 +1 @@ +Location sharing: Add cell for live location sharing in timeline From d96f204ad88f1fd1d880d0c6718ec296bceab713 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 22 Apr 2022 11:50:34 +0200 Subject: [PATCH 060/158] MXKAppSettings: Add beacon info state event as part of room message to enables beacon display on timeline. --- Riot/Modules/MatrixKit/Models/MXKAppSettings.m | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Riot/Modules/MatrixKit/Models/MXKAppSettings.m b/Riot/Modules/MatrixKit/Models/MXKAppSettings.m index b31fc0658..8d8e44b12 100644 --- a/Riot/Modules/MatrixKit/Models/MXKAppSettings.m +++ b/Riot/Modules/MatrixKit/Models/MXKAppSettings.m @@ -147,7 +147,9 @@ static NSString *const kMXAppGroupID = @"group.org.matrix"; kMXEventTypeStringKeyVerificationCancel, kMXEventTypeStringKeyVerificationDone, kMXEventTypeStringPollStart, - kMXEventTypeStringPollStartMSC3381 + kMXEventTypeStringPollStartMSC3381, + kMXEventTypeStringBeaconInfo, + kMXEventTypeStringBeaconInfoMSC3672 ].mutableCopy; @@ -179,7 +181,9 @@ static NSString *const kMXAppGroupID = @"group.org.matrix"; kMXEventTypeStringKeyVerificationCancel, kMXEventTypeStringKeyVerificationDone, kMXEventTypeStringPollStart, - kMXEventTypeStringPollStartMSC3381 + kMXEventTypeStringPollStartMSC3381, + kMXEventTypeStringBeaconInfo, + kMXEventTypeStringBeaconInfoMSC3672 ].mutableCopy; lastMessageEventTypesAllowList = @[ From 2cc526bd5d5aabdce810af082bc8e7c32b20417d Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 22 Apr 2022 11:51:22 +0200 Subject: [PATCH 061/158] RoomBubbleCellData: Handle live location cell and beacon info summary. --- .../Room/CellData/RoomBubbleCellData.h | 7 +++- .../Room/CellData/RoomBubbleCellData.m | 36 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.h b/Riot/Modules/Room/CellData/RoomBubbleCellData.h index 706776efb..aa9c661b1 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.h +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.h @@ -16,6 +16,8 @@ #import "MatrixKit.h" +@protocol MXBeaconInfoSummaryProtocol; + extern NSString *const URLPreviewDidUpdateNotification; // Custom tags for MXKRoomBubbleCellDataStoring.tag @@ -33,7 +35,8 @@ typedef NS_ENUM(NSInteger, RoomBubbleCellDataTag) RoomBubbleCellDataTagGroupCall, RoomBubbleCellDataTagRoomCreationIntro, RoomBubbleCellDataTagPoll, - RoomBubbleCellDataTagLocation + RoomBubbleCellDataTagLocation, + RoomBubbleCellDataTagLiveLocation }; /** @@ -92,6 +95,8 @@ typedef NS_ENUM(NSInteger, RoomBubbleCellDataTag) */ @property(nonatomic) BOOL isKeyVerificationOperationPending; +@property(nonatomic, strong, readonly) id beaconInfoSummary; + /** Index of the component which needs a sent tick displayed. -1 if none. */ diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index 70ad44a48..db311b8fe 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -38,6 +38,8 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat // Flags to "Show All" reactions for an event @property(nonatomic) NSMutableSet *eventsToShowAllReactions; +@property(nonatomic, strong, readwrite) id beaconInfoSummary; + @end @implementation RoomBubbleCellData @@ -159,6 +161,15 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat break; } + case MXEventTypeBeaconInfo: + { + self.tag = RoomBubbleCellDataTagLiveLocation; + self.collapsable = NO; + self.collapsed = NO; + + [self updateBeaconInfoSummaryWithEventId:event.eventId]; + } + break; case MXEventTypeCustom: { if ([event.type isEqualToString:kWidgetMatrixEventTypeString] @@ -210,6 +221,11 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat // Update any URL preview data as necessary. [self refreshURLPreviewForEventId:event.eventId]; + + if (self.tag == RoomBubbleCellDataTagLiveLocation) + { + [self updateBeaconInfoSummaryWithEventId:eventId]; + } return retVal; } @@ -279,6 +295,17 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat return NO; } + if (self.tag == RoomBubbleCellDataTagLiveLocation) + { + // If the summary does not exist do show the cell + if (!self.beaconInfoSummary) + { + return YES; + } + + return NO; + } + return [super hasNoDisplay]; } @@ -983,6 +1010,9 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat case RoomBubbleCellDataTagLocation: shouldAddEvent = NO; break; + case RoomBubbleCellDataTagLiveLocation: + shouldAddEvent = NO; + break; default: break; } @@ -1294,5 +1324,11 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat }]; } +- (void)updateBeaconInfoSummaryWithEventId:(NSString *)eventId +{ + MXBeaconInfoSummary *beaconInfoSummary = [self.mxSession.aggregations.beaconAggegations beaconInfoSummaryFor:eventId inRoomWithId:self.roomId]; + + self.beaconInfoSummary = beaconInfoSummary; +} @end From e6621bcc19a2ac82895e27ebc37e8791a88e5318 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 22 Apr 2022 11:51:45 +0200 Subject: [PATCH 062/158] RoomVC: Handle live location sharing cell data. --- Riot/Modules/Room/RoomViewController.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index e12eaae98..b941df182 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -2905,7 +2905,7 @@ static CGSize kThreadListBarButtonItemImageSize; } } } - else if (bubbleData.tag == RoomBubbleCellDataTagLocation) + else if (bubbleData.tag == RoomBubbleCellDataTagLocation || bubbleData.tag == RoomBubbleCellDataTagLiveLocation) { if (bubbleData.isIncoming) { From 98fcb2d2739fdbe46bf545a3e940e713ddc59542 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 22 Apr 2022 11:55:40 +0200 Subject: [PATCH 063/158] Update changes --- changelog.d/6057.wip | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6057.wip diff --git a/changelog.d/6057.wip b/changelog.d/6057.wip new file mode 100644 index 000000000..004bcbfb3 --- /dev/null +++ b/changelog.d/6057.wip @@ -0,0 +1 @@ +Location sharing: Support live location event in the timeline. \ No newline at end of file From 6ae8f9f719ed64ed281de84f41db24de645cf657 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 22 Apr 2022 12:34:34 +0200 Subject: [PATCH 064/158] Update strings --- Riot/Generated/Strings.swift | 124 +++++++++++++++++++++++++++++++---- 1 file changed, 111 insertions(+), 13 deletions(-) diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 9969e67de..7aacdbb01 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -3807,6 +3807,74 @@ public class VectorL10n: NSObject { public static var on: String { return VectorL10n.tr("Vector", "on") } + /// Profile picture + public static var onboardingAvatarAccessibilityLabel: String { + return VectorL10n.tr("Vector", "onboarding_avatar_accessibility_label") + } + /// You can change this anytime. + public static var onboardingAvatarMessage: String { + return VectorL10n.tr("Vector", "onboarding_avatar_message") + } + /// Add a profile picture + public static var onboardingAvatarTitle: String { + return VectorL10n.tr("Vector", "onboarding_avatar_title") + } + /// Let's go + public static var onboardingCelebrationButton: String { + return VectorL10n.tr("Vector", "onboarding_celebration_button") + } + /// Your preferences have been saved. + public static var onboardingCelebrationMessage: String { + return VectorL10n.tr("Vector", "onboarding_celebration_message") + } + /// You’re all set! + public static var onboardingCelebrationTitle: String { + return VectorL10n.tr("Vector", "onboarding_celebration_title") + } + /// Take me home + public static var onboardingCongratulationsHomeButton: String { + return VectorL10n.tr("Vector", "onboarding_congratulations_home_button") + } + /// Your account %@ has been created. + public static func onboardingCongratulationsMessage(_ p1: String) -> String { + return VectorL10n.tr("Vector", "onboarding_congratulations_message", p1) + } + /// Personalise profile + public static var onboardingCongratulationsPersonalizeButton: String { + return VectorL10n.tr("Vector", "onboarding_congratulations_personalize_button") + } + /// Congratulations! + public static var onboardingCongratulationsTitle: String { + return VectorL10n.tr("Vector", "onboarding_congratulations_title") + } + /// You can change this later + public static var onboardingDisplayNameHint: String { + return VectorL10n.tr("Vector", "onboarding_display_name_hint") + } + /// Your display name must be less than 256 characters + public static var onboardingDisplayNameMaxLength: String { + return VectorL10n.tr("Vector", "onboarding_display_name_max_length") + } + /// This will be shown when you send messages. + public static var onboardingDisplayNameMessage: String { + return VectorL10n.tr("Vector", "onboarding_display_name_message") + } + /// Display Name + public static var onboardingDisplayNamePlaceholder: String { + return VectorL10n.tr("Vector", "onboarding_display_name_placeholder") + } + /// Choose a display name + public static var onboardingDisplayNameTitle: String { + return VectorL10n.tr("Vector", "onboarding_display_name_title") + } + /// Save and continue + public static var onboardingPersonalizationSave: String { + return VectorL10n.tr("Vector", "onboarding_personalization_save") + } + /// Skip this step + public static var onboardingPersonalizationSkip: String { + return VectorL10n.tr("Vector", "onboarding_personalization_skip") + } /// I already have an account public static var onboardingSplashLoginButtonTitle: String { return VectorL10n.tr("Vector", "onboarding_splash_login_button_title") @@ -4243,9 +4311,17 @@ public class VectorL10n: NSObject { public static var roomAccessSettingsScreenUpgradeAlertAutoInviteSwitch: String { return VectorL10n.tr("Vector", "room_access_settings_screen_upgrade_alert_auto_invite_switch") } - /// Anyone in Space name will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime.\n\nPlease note upgrading will make a new version of the room. All current messages will stay in this archived room. - public static var roomAccessSettingsScreenUpgradeAlertMessage: String { - return VectorL10n.tr("Vector", "room_access_settings_screen_upgrade_alert_message") + /// Anyone in %@ will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime. + public static func roomAccessSettingsScreenUpgradeAlertMessage(_ p1: String) -> String { + return VectorL10n.tr("Vector", "room_access_settings_screen_upgrade_alert_message", p1) + } + /// Anyone in a parent space will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime. + public static var roomAccessSettingsScreenUpgradeAlertMessageNoParam: String { + return VectorL10n.tr("Vector", "room_access_settings_screen_upgrade_alert_message_no_param") + } + /// Please note upgrading will make a new version of the room. All current messages will stay in this archived room. + public static var roomAccessSettingsScreenUpgradeAlertNote: String { + return VectorL10n.tr("Vector", "room_access_settings_screen_upgrade_alert_note") } /// Upgrade room public static var roomAccessSettingsScreenUpgradeAlertTitle: String { @@ -6859,6 +6935,18 @@ public class VectorL10n: NSObject { public static var settingsPinRoomsWithUnread: String { return VectorL10n.tr("Vector", "settings_pin_rooms_with_unread") } + /// Presence + public static var settingsPresence: String { + return VectorL10n.tr("Vector", "settings_presence") + } + /// Offline Mode + public static var settingsPresenceOfflineMode: String { + return VectorL10n.tr("Vector", "settings_presence_offline_mode") + } + /// If enabled, you will always appear offline to other users, even when using the application. + public static var settingsPresenceOfflineModeDescription: String { + return VectorL10n.tr("Vector", "settings_presence_offline_mode_description") + } /// Privacy Policy public static var settingsPrivacyPolicy: String { return VectorL10n.tr("Vector", "settings_privacy_policy") @@ -7643,6 +7731,14 @@ public class VectorL10n: NSObject { public static var threadsBetaTitle: String { return VectorL10n.tr("Vector", "threads_beta_title") } + /// Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. + public static var threadsDiscourageInformation1: String { + return VectorL10n.tr("Vector", "threads_discourage_information_1") + } + /// \n\nDo you want to enable threads anyway? + public static var threadsDiscourageInformation2: String { + return VectorL10n.tr("Vector", "threads_discourage_information_2") + } /// Threads help keep your conversations on-topic and easy to track. public static var threadsEmptyInfoAll: String { return VectorL10n.tr("Vector", "threads_empty_info_all") @@ -8038,16 +8134,18 @@ public class VectorL10n: NSObject { extension VectorL10n { static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String { - let format = NSLocalizedString(key, tableName: table, bundle: Bundle(for: BundleToken.self), comment: "") - let locale: Locale - if let providedLocale = LocaleProvider.locale { - locale = providedLocale - } else { - locale = Locale.current - } - - return String(format: format, locale: locale, arguments: args) + let format = NSLocalizedString(key, tableName: table, bundle: bundle, comment: "") + let locale = LocaleProvider.locale ?? Locale.current + return String(format: format, locale: locale, arguments: args) + } + /// The bundle to load strings from. This will be the app's bundle unless running + /// the UI tests target, in which case the strings are contained in the tests bundle. + static let bundle: Bundle = { + if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil { + // The tests bundle is embedded inside a runner. Find the bundle for VectorL10n. + return Bundle(for: VectorL10n.self) } + return Bundle.app + }() } -private final class BundleToken {} From 98a6599d6b22d82b563aaf4b8a70e6e018f07a99 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 22 Apr 2022 12:35:04 +0200 Subject: [PATCH 065/158] Update StaticLocationViewingViewModel, MapViewErrorAlertInfoBuilder usage; --- .../StaticLocationViewingViewModel.swift | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/Room/StaticLocationSharingViewer/StaticLocationViewingViewModel.swift b/RiotSwiftUI/Modules/Room/StaticLocationSharingViewer/StaticLocationViewingViewModel.swift index 17af5c4cb..508aa6631 100644 --- a/RiotSwiftUI/Modules/Room/StaticLocationSharingViewer/StaticLocationViewingViewModel.swift +++ b/RiotSwiftUI/Modules/Room/StaticLocationSharingViewer/StaticLocationViewingViewModel.swift @@ -78,7 +78,17 @@ class StaticLocationViewingViewModel: StaticLocationViewingViewModelType, Static } let alertInfo = mapViewErrorAlertInfoBuilder.build(with: error) { [weak self] in - self?.completion?(.close) + + switch error { + case .invalidLocationAuthorization: + if let applicationSettingsURL = URL(string:UIApplication.openSettingsURLString) { + UIApplication.shared.open(applicationSettingsURL) + } else { + self?.completion?(.close) + } + default: + self?.completion?(.close) + } } state.bindings.alertInfo = alertInfo From 6740d93de420495f19b0627149f50eab11f6c824 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 22 Apr 2022 12:36:40 +0200 Subject: [PATCH 066/158] Podfile: Move to iOS 14. Install DSBottomSheet. --- Podfile | 2 +- Podfile.lock | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Podfile b/Podfile index 86ce64be9..1ab2e77df 100644 --- a/Podfile +++ b/Podfile @@ -1,7 +1,7 @@ source 'https://cdn.cocoapods.org/' # Uncomment this line to define a global platform for your project -platform :ios, '13.0' +platform :ios, '14.0' # Use frameworks to allow usage of pods written in Swift use_frameworks! diff --git a/Podfile.lock b/Podfile.lock index 76519dbe5..1cc3bd2e1 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -20,6 +20,7 @@ PODS: - BlueRSA (1.0.200) - DGCollectionViewLeftAlignFlowLayout (1.0.4) - Down (0.11.0) + - DSBottomSheet (0.3.0) - DSWaveformImage (6.1.1) - DTCoreText (1.6.26): - DTCoreText/Core (= 1.6.26) @@ -105,6 +106,7 @@ DEPENDENCIES: - AnalyticsEvents (from `https://github.com/matrix-org/matrix-analytics-events.git`, branch `release/swift`) - DGCollectionViewLeftAlignFlowLayout (~> 1.0.4) - Down (~> 0.11.0) + - DSBottomSheet (~> 0.3) - DSWaveformImage (~> 6.1.1) - DTCoreText (~> 1.6.25) - ffmpeg-kit-ios-audio (= 4.5.1) @@ -139,6 +141,7 @@ SPEC REPOS: - BlueRSA - DGCollectionViewLeftAlignFlowLayout - Down + - DSBottomSheet - DSWaveformImage - DTCoreText - DTFoundation @@ -191,6 +194,7 @@ SPEC CHECKSUMS: BlueRSA: dfeef51db96bcc4edec654956c1581adbda4e6a3 DGCollectionViewLeftAlignFlowLayout: a0fa58797373ded039cafba8133e79373d048399 Down: b6ba1bc985c9d2f4e15e3b293d2207766fa12612 + DSBottomSheet: ca0ac37eb5af2dd54663f86b84382ed90a59be2a DSWaveformImage: 3c718a0cf99291887ee70d1d0c18d80101d3d9ce DTCoreText: ec749e013f2e1f76de5e7c7634642e600a7467ce DTFoundation: a53f8cda2489208cbc71c648be177f902ee17536 @@ -225,6 +229,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 8055bae15b82bd29b1d12c64ff038fe6f86a8ca0 +PODFILE CHECKSUM: 39feedaf75b9a9287e4fe5309200e0493a9251a3 -COCOAPODS: 1.11.2 +COCOAPODS: 1.11.3 From 95bf93f14a741e00687fedbb0815f9a51706bce0 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 22 Apr 2022 12:58:40 +0200 Subject: [PATCH 067/158] Project.xcconfig: Update deployment target. --- Config/Project.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Config/Project.xcconfig b/Config/Project.xcconfig index 331f7b44f..7d21d78a5 100644 --- a/Config/Project.xcconfig +++ b/Config/Project.xcconfig @@ -25,7 +25,7 @@ KEYCHAIN_ACCESS_GROUP = $(AppIdentifierPrefix)$(BASE_BUNDLE_IDENTIFIER).keychain.shared // Build settings -IPHONEOS_DEPLOYMENT_TARGET = 12.1 +IPHONEOS_DEPLOYMENT_TARGET = 14.0 SDKROOT = iphoneos TARGETED_DEVICE_FAMILY = 1,2 SWIFT_VERSION = 5.3.1 From 7b31cef58183922b75f9aa75d1c0a16754c4067a Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 22 Apr 2022 13:01:27 +0200 Subject: [PATCH 068/158] Third party licenses: Add DSBottomSheet. --- Riot/Assets/third_party_licenses.html | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Riot/Assets/third_party_licenses.html b/Riot/Assets/third_party_licenses.html index 8d973a9a6..bfbc72922 100644 --- a/Riot/Assets/third_party_licenses.html +++ b/Riot/Assets/third_party_licenses.html @@ -1950,6 +1950,31 @@ Library. CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+
  • + DSBottomSheet (https://github.com/danielsaidi/BottomSheet) +

    + MIT License +

    + Copyright (c) 2021 Daniel Saidi +

    + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: +

    + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. +

    + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +

    +
  • From 6317cd63f8793251cea7a13d352b0fca80704928 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 22 Apr 2022 13:10:43 +0200 Subject: [PATCH 069/158] Project.xcconfig: Update to Swift 5.6. --- Config/Project.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Config/Project.xcconfig b/Config/Project.xcconfig index 7d21d78a5..95b6295d6 100644 --- a/Config/Project.xcconfig +++ b/Config/Project.xcconfig @@ -28,7 +28,7 @@ KEYCHAIN_ACCESS_GROUP = $(AppIdentifierPrefix)$(BASE_BUNDLE_IDENTIFIER).keychain IPHONEOS_DEPLOYMENT_TARGET = 14.0 SDKROOT = iphoneos TARGETED_DEVICE_FAMILY = 1,2 -SWIFT_VERSION = 5.3.1 +SWIFT_VERSION = 5.6 ENABLE_BITCODE = NO LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @executable_path/../../Frameworks From 89d7fd501e21833030ce0b32e1c8ea11e8b93fea Mon Sep 17 00:00:00 2001 From: Kat Gerasimova Date: Fri, 22 Apr 2022 11:14:19 +0100 Subject: [PATCH 070/158] Update issue labelling automation Don't label A-Threads with Z-Labs any more because threads have been released --- .github/workflows/triage-move-labelled.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/triage-move-labelled.yml b/.github/workflows/triage-move-labelled.yml index d1e110540..994a13941 100644 --- a/.github/workflows/triage-move-labelled.yml +++ b/.github/workflows/triage-move-labelled.yml @@ -11,7 +11,6 @@ jobs: if: > contains(github.event.issue.labels.*.name, 'A-Maths') || contains(github.event.issue.labels.*.name, 'A-Message-Pinning') || - contains(github.event.issue.labels.*.name, 'A-Threads') || contains(github.event.issue.labels.*.name, 'A-Polls') || contains(github.event.issue.labels.*.name, 'A-Location-Sharing') || contains(github.event.issue.labels.*.name, 'A-Message-Bubbles') || From f2354b0fed7a8caefc6e98030fa0519cc753abd6 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 22 Apr 2022 15:23:36 +0200 Subject: [PATCH 071/158] Fix SwiftUI tests --- RiotSwiftUI/targetUITests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/RiotSwiftUI/targetUITests.yml b/RiotSwiftUI/targetUITests.yml index 86e38b532..b7a0f9d00 100644 --- a/RiotSwiftUI/targetUITests.yml +++ b/RiotSwiftUI/targetUITests.yml @@ -61,6 +61,7 @@ targets: - path: ../Riot/Categories/Character.swift - path: ../Riot/Categories/UIColor.swift - path: ../Riot/Categories/UISearchBar.swift + - path: ../Riot/Categories/UIView.swift - path: ../Riot/Assets/en.lproj/Vector.strings - path: ../Riot/Modules/Analytics/AnalyticsScreen.swift - path: ../Riot/Assets/en.lproj/Untranslated.strings From 29d824ff3ad295ceeb3812a171ba011410a455b6 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 22 Apr 2022 17:57:54 +0200 Subject: [PATCH 072/158] Update Riot/Modules/Room/CellData/RoomBubbleCellData.m Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --- Riot/Modules/Room/CellData/RoomBubbleCellData.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index db311b8fe..91a78d4cc 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -297,7 +297,7 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat if (self.tag == RoomBubbleCellDataTagLiveLocation) { - // If the summary does not exist do show the cell + // If the summary does not exist don't show the cell if (!self.beaconInfoSummary) { return YES; From 4449637264d2e78c4f7fc55870f7cc0686af1dd6 Mon Sep 17 00:00:00 2001 From: SBiOSoftWhare Date: Fri, 22 Apr 2022 17:59:15 +0200 Subject: [PATCH 073/158] Update Riot/Modules/Room/CellData/RoomBubbleCellData.m Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --- Riot/Modules/Room/CellData/RoomBubbleCellData.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index 91a78d4cc..dd9452a43 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -1326,7 +1326,7 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat - (void)updateBeaconInfoSummaryWithEventId:(NSString *)eventId { - MXBeaconInfoSummary *beaconInfoSummary = [self.mxSession.aggregations.beaconAggegations beaconInfoSummaryFor:eventId inRoomWithId:self.roomId]; + MXBeaconInfoSummary *beaconInfoSummary = [self.mxSession.aggregations.beaconAggegations beaconInfoSummaryFor:eventId inRoomWithId:self.roomId]; self.beaconInfoSummary = beaconInfoSummary; } From 186a1c0ec271a27631654db58d93adcd5e973310 Mon Sep 17 00:00:00 2001 From: MaximeE Date: Fri, 22 Apr 2022 18:04:19 +0200 Subject: [PATCH 074/158] 6029: Refacto how to handle all state of the live location cell --- .../Contents.json | 23 ++ .../Group 1.png | Bin 0 -> 5239 bytes .../Group 1@2x.png | Bin 0 -> 12341 bytes .../Group 1@3x.png | Bin 0 -> 22767 bytes .../Contents.json | 26 +++ .../Subtract.png | Bin 0 -> 1111 bytes .../Subtract@2x.png | Bin 0 -> 2229 bytes .../Subtract@3x.png | Bin 0 -> 3268 bytes .../Contents.json | 23 ++ .../Group 1.png | Bin 0 -> 4862 bytes .../Group 1@2x.png | Bin 0 -> 12255 bytes .../Group 1@3x.png | Bin 0 -> 22868 bytes Riot/Assets/en.lproj/Vector.strings | 2 + Riot/Generated/Images.swift | 3 + Riot/Generated/Strings.swift | 8 + Riot/Managers/Theme/Theme.swift | 10 + Riot/Managers/Theme/Themes/DarkTheme.swift | 8 + Riot/Managers/Theme/Themes/DefaultTheme.swift | 8 + .../Location/RoomTimelineLocationView.swift | 218 +++++++++++++++--- .../Location/RoomTimelineLocationView.xib | 18 +- Riot/Modules/Room/MXKRoomViewController.m | 16 ++ .../Common/MXKRoomBubbleTableViewCell.h | 14 ++ .../Common/MXKRoomBubbleTableViewCell.m | 3 + .../Cells/Location/LocationPlainCell.swift | 61 +++-- 24 files changed, 368 insertions(+), 73 deletions(-) create mode 100644 Riot/Assets/Images.xcassets/Room/Location/location_live_cell_ended_image.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/Location/location_live_cell_ended_image.imageset/Group 1.png create mode 100644 Riot/Assets/Images.xcassets/Room/Location/location_live_cell_ended_image.imageset/Group 1@2x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Location/location_live_cell_ended_image.imageset/Group 1@3x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Location/location_live_cell_icon.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/Location/location_live_cell_icon.imageset/Subtract.png create mode 100644 Riot/Assets/Images.xcassets/Room/Location/location_live_cell_icon.imageset/Subtract@2x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Location/location_live_cell_icon.imageset/Subtract@3x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Location/location_live_cell_loading_image.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/Location/location_live_cell_loading_image.imageset/Group 1.png create mode 100644 Riot/Assets/Images.xcassets/Room/Location/location_live_cell_loading_image.imageset/Group 1@2x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Location/location_live_cell_loading_image.imageset/Group 1@3x.png diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_ended_image.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_ended_image.imageset/Contents.json new file mode 100644 index 000000000..ad6d3af55 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_ended_image.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Group 1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group 1@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group 1@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_ended_image.imageset/Group 1.png b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_ended_image.imageset/Group 1.png new file mode 100644 index 0000000000000000000000000000000000000000..d1087e6b3c5a36573d065ccc1c5fd6b504988b64 GIT binary patch literal 5239 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 literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_ended_image.imageset/Group 1@2x.png b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_ended_image.imageset/Group 1@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ffad58d654877f99813d23db93228a8bfa58dee2 GIT binary patch literal 12341 zcmXwf2{hF2_y3Hgv6B+AjlB?3Xsiv%uA-T-3{oUi7-V0vmQsUI4C;f#Fl33bf2hn* zlk!29tYgm-*_Zh}^ZlQ{b2?7m&vx%~@9WC%GUH2(P)>C0huD#Sa33 zUf?(a{xZBuGX%dl{mdK!AdnOC%zsdGTe(&6M`(bpsS$)k6Q2kFVe>>mkskUyp;uIx|-OERekYj#|I!H*!pyeqT-dqq}~{Fr1nue9^U0cycc^ z)!ji*K9#M37`A|ZgTxPA6u20Si8kbv8KgcvW^Bap^Zn)I;5;yW zPPgZ?LLfimtvsGTZO?Fb*cL0+U`soY$J`vZVW6E4y`BIEibzLlWsa=$uy{QC&*b74M8)w637w+o(9yc<128Tf) zq-02)8<|J%NBTX z%9YfXTya{@`oW^xA}mzQy4*9%u%`mf7I8OMfzK#n|z4ERs9pcqP3BJ{G9aJonc1ahOE!%zP^8+mo@U+XKy z=qcM9;JS{)Lk7-$-94HiRzhCo>#KrCkZmIIZ95c4crV1P;Bt`Gl%zv)1#? z3`ad1fTY<}%%_biA^9LNhZ<8=wrfuZH2U5h4}ma^IV(jhfo7HgF%2IJZYgzQ*pi(Iq`1)_BWT zO~nOzE`6v$JvKx)9cZX|m3I1iXku0X(q=E&I!{|0dP+_X+v}m>!4&cDXV}1_FEE5? zV2|S!rqm^5ak@AwKLirkZxlXGhq@%3VW33()GwbSXTMXRP*us5BK7~1U1!GPNyIc% zQuV5ua*-#}<|tiI&bK&qE5!Y}G%LRWC4;FT9k=-$5n3Ff?45cwT+x`{om>Bfs#N5O+UuKLk%~U`l!S zvm*<(;1yQWCX>a7_F2A+T&m25(9OjH8}NDG4^dQIPV4SaMHGkznxQywGfgszE4sk6 z%WGlvmq+9W^!__1H4`Wj2B#Exp;Om0F3c+91Sh@9Ss;}s#kl>}f7jGH?!qy5VzFKB zuT7>uw|4sDf{cJQjSo3;`lSpRQWZ1!aVRX(R>T-@`QKe!2Jn)$+S-%jCxJaFw8&&7 zz!h~n@p(^>BtARh2Zb+Ch@Vg{hu^w|(;i0D(Kl?!#U$(|zWYVVIjQl<9#&;Da5}45 z7OZR;yrjIUrSEp~2uvpyn=D81nszn#;*gUhB!}x@WomhA@ovm9++|YQJ;pJ{t;={z zR6r6*%Qooi^nSoU_e2HGRPAEC!*NvFWaS@KGR6I&9aWeF2xa=7KiFEXD~rf z$PrQ&lyo!zD-lZw`ttxre)~j7Zax*RxZ?fxKv`R93rR!7ibHO^43ii?DM&9YUs&+M zr%y8kXY7$_1pCzWl~9G1o%57FrbF>q3PMR|xp9ZHc7Ls#2 ztq!DBhnTb1C+f9!uoZ0ZhjHQ#6|0WFgl9>H8WxeWyXmEB-3Vf&Q=cGkXLC!9wy#gp ziGp(x;eDTnCER&AJLx~tJq79e&ujUBvwowzk7a}4v+1l@um@Bgf*;h$O3@Am?bbyf`33yRmSlOAs8?7KaAruch2FocMEN5eB<2HH1}QoBrf!CK&bGv#tU!@h z`hbqar)Ocm^|Spin%r3D@Ke5NjC$K3Ko5L-K{4xP{Tzb3PWL}I@1QUn5}HR30?|r@ z)ZJW1lDsN#HmdqxQzU$I({s?%z#$Y_MzZ7K#dHE8`dyNM@POUbRLd&?Hl(zFPxzqF z|APr15F5{l4c%2GywZ3E&#DTs&bp??dzhUqYh@S)qUgwdj>lGL1uQOhaGPP^TgVU(Z z-pg#T+X-ZUd@Bpsw>cO^Wo=4JeoEYD)jxs=9q%MI92b;XDW>8)*7U5(EQI=*MrjFe z3o<)KFOJe%g0dCKWA zg9>q=6hA1GlmzZvj$-bEI!3pZU%BnJW(j-}1`*L=!G5E5G^GKkA9*TGDJtc}wNOiI z$|T=wC58Ygcm$!UM>WWIEIDyq82VFRu$yh)2-#6YYE+r108mLpoCSOHrf2P1pQ!8m zSKPP}a(14d4QX}dEZF%-5+v&s;)N2V(X6Xw3KfoYK znKrk}l2hqG1EriuRexL-sfk{OMFBW@?aSd8VtI_NU_eQ6XE#Rz&?aEzDbR~!0^_U5 zV~cTw4I#1~=^eh5_s2OYdOL9cmf`FG9Ha-uc0r{uuvV#a`ZukwUPPqC3yr_C6o<`K zc0wGHguIU9@T(UT4e_g5ay`K+9QyM9rLh0Sj25Nhd&|nWKZ!7KJ%OxHK8%8Aq0%dC z?zU952tDvKSK5E)gXb0;Ol+8-s1plGD*6gT0I2UbpzI`zn~-Zhiq9Go7>`wJe$=9A zT^e`9mO*&ED+%Qr(Bj6uQMuJYdf$HMh@@aI8-h$O*i<7=Au%mQ%XjSsn!J2%Y(Cgr zp?$E~@9x(!)R*wxxmt2>x`we5IN@_C&!^b>zNyKzUA2?bn#!h=^o6jvkw1SHb1@ZTzgwc#Z+9luE_F*U>D(XrsXsXU zOaFkL-kg4L&}_#Jk;p9heH6;)Ba2kc>d#S0`5Fi`(lGM(0j#k-nGpX$Snb(XRQR8@ z870=?B(sd=&!Z{nPOyB*V>6IIFi%W&(Ug9dK%2@XnhmH>`cw(2jSDw#I1TR4SG4~6 z`(`PSE>(Uw#fXN?*@A&*>c!_e=K>4cDLHA{~}Y92w7`r`!@mr$|TH8~3P;I4hJcH|&D=^Gdm55_oi;6hKFprlRzIM0pK_80PR&jpxut_yeSM(=e7n@;7SevO)z`fsgB=p!!F?_7qq9T6Ds@dD9~ z5FNEORc>mi-%=zP7_vn>NBa!@$;Rn7@-c>%mLuUVk?^1vCrDq@{l#J3tecBP-^>oo_3J!shI>rjhwo92iKDqu+(rMw-uFdd5JvcfMW_4ZazR*W4k zuBZ3*qJtamCD&YMtnO7rjbp`Qf>OD}#ZceHCRQ6OK3kWerp${Jik_sW%&BntM(?b& z8j4ti980fKIXpCE?B3MN>zRO5YO!7T`-}}SsEBrynl!=_+c%>FEsu@bilXbejKGqz zZExxdw@<_oD!NeU3?U&hNdy<-BQOamI|k)z7FNRbCkSYtj>IeH5Oa<@!dbbE`F(jg z{TbT>TU+!Goa_+gk4aoZmrGM{0*fGCH1{5|UUexgzj4u4cnAC=^Ek z3T2n-^e~#S+Ixo997*8O^poUkQxX_gH|zJ*;>1l=@dzPeN7kaE_5&I7)xvvR+^Oq+ zHMUT!FqAJ&A$92{{z*BiYGkq{`Y-qFDCrSeDQ0n!mF+5s)Y5 zjS{XnU84;CzLq~=L%kT2Zx9`7JTdgH9cq69%2zK#^*QGHSnT-}g51?#T{?1ge}Da$ zQD?H5kPWm==;(Ni{3mLkjupuwJm?hChVim9d}t!-IoLwf!XZ&$%g`Gk*iI;;o25 zgQy+vt(|U6t`FmIf16qkK*^K~a^g}9EBYNLPT@I{Hdhl%s5n2<+I(TuPD1+MrP1$8 zv#ukC5vp!W&Wu+}<>eMGu>9rx(mm5}SH97bh1Fb_C@hnt>w;IVeEZ<>5-GNH9HJ5f zft5W}fuc#a{kOX?v1y#RR)g@-SF4-jqZ8Hq>~&j%gQKd`8#@!Zw;V3*&lPu;kI6~j z`)~G}>+-Lb^}+WnG3)~4F`8*ql=7l!nXSqF7iSOnaNnZB2g729PYO*(AMOMj?x5%G zoqGpdN251)Cd#K;_cvDfhe)5BbwaBHoQ{q|R1&3+++b~|m*$e-rx!<;)4~IA4MmP} z6HAQ4e8a=a_egZ)dXniH!>M*|x*L;!3Sut+<@@>C0WX#{K|}fcI!_5Qq)N2v z40=~v^u=oE4;|X z^uZt5UxUKq*eafl{Z@k5L!f+jUJ=s8vPkR5=7)j2n}x=Sz1@cXm+@AuE`PC3!m*K8C<{Gd0a*)F^;D3sAIQQDZf`;S74 z(!gzeI(rLkC?(p(Ox|j-&IJZBbWnI=&wCVcWJ%vsk1h7 zIQrO|`fC`bhT>jm=pS~vd{6i?NFbW{p?q_x3X~LM{4h3+==iSXaffc5qto5++3G8< z#1n*+Z(I(4?^=;Sc|4kwBN8K-CM36(ri(E>gyzpKMz&!*ics8}==#d7uIQ!a2ZuvA zMep@>5jtJxg9_Vb6|NNb3n9$;S|6OJ(|!ES$*ea~cp@CX^plcDj;v&OpiUqDrQ$+}8yCR+-JFRdY@!fj$vm*hfQ2RV z#|TyKhxcF1U(F!yi}~jAuRfE56+VN&i%PQ)(%B> z`8e9+h{E0f*4laKnNgmxjv}YQVRBEvEY%Nv-eb3k9X-a!=v7`Dw~y*N6{NVHL7_<* z)PYdN;$pGZ$Vr{(ik!_-04L9D@zg#rweS%H#$#r0BHbs-xV4H+_{h zxPMnqE2sYBq4P7>+Uh3zoW(JS|ESA`G{}Rit(3bFpmfIb*nEPp%6S&Ht0G=u&nSBd zMfsgw-P}YgXEho|?d~7zOJkZpr(eQZbUD7}%QR2nZ|)aGZT`3n>WSAY9DZ#v)ZL>9 zHKEv%LM~;q5#894&CPciNP77iw|M_cqlLAvh5oOs{Cg44y8HBmQj7ro z`b%v2iw}c=I8E+*==aslzbx37xBGH_Ri7KX{A={C*v8+#W%$+)-v2f@eV%v#^UAjj zM-k__(!vTb)zEryi;&xNQI~{G?;=2ffOu&_>znx2UCvg@&N!SXcJl5R+V)A#&HZ{t z4cH8doci6{)7{9#ruw|V950J_OdTk2M!g(6-(K#L`6mgq_eWyMuzYHHc?*c zT{`hLayD)R?Y9410ZTpddU)HJYrGkdQFcyFPUX(`HtM1yg5PP;)ffj`uu^#gx%^U4 z$&v2RsMI!N$*peaoFv%gd0Ou^5S2Yk<*S}A`7-topE*|lFQ55SsB+zMbL)ZAUib+9 z)^ptESvJI&ql9KUk?UfX`bWqOY3&EWPR-cEtj<glNbOZq6P_pvx08M8D!bE>C*3ROc@-Scnk$`3yih@zcH4NG;iOOCZDbuZVz~=~VOzVwK_FwJqV?dx zYsby~Pk%-kdy8Wqo;_P`+Fm>S9uQ>W;V^BuzrQ_RoFA=^S8lux8`=5YJ5Z`vxOU#=$OGKQt@luw4mMmUr z!v-QYZaw`#+nLL6UG8nQ5L2eR-4JPm_66g?s-2@oKJB%r7s+Wu30OVpzzDD-f6j%xS$0u*;&6=78BQ z?m++4-y}O$VFe5)t}WyJWwtik&8;n)#;X77o`1M(GT1rYyeJrleH)m2I%h`!(^x5B ze}c98AzO%;R1+lg!E8SiRWGj};rG{S;!Jfic7o`yL0*WKMB`6E02c#BfP@{F4WonRGmaWRfUa|H{oDx88oJ*mx_UNK=Shel_)+ z+v!R+M7wxmJW$h+fuSD#;$dte7^DynpH=q9UB2H<(R2>o4q!v@FioxYT7uKlGQOSt zRoZxPYk8bk>`peO{1t;A#^;P^8MWA0Z7}%}rYC^bnsqa$v zs1ThaVD2oqij3X;WoISptgDfW&lD(2cZJ{AmWqa4Gz2Ord#cB3XxJ3gWFmAwalt8A z$mHz@(?6xE3YhQOU>o98e$)_sVUZECdP(~AZhs`MbD{q{g=%{P41h7uF_j-aarb+! z6U=whkbHK#+3)(;u1W03ov;q$GRo=K)j*ACqr9N^xTE=Dt_Ir?s~L()sRu8vDC2Ys zjD$XaiLz#OHi}`b6rA|SzrZs(Vs-v-9#8ZO_LXXSVNq!03y5b{ekW_xGkDT6oT-+b z1*fUSik6VlBMXL-qf@_~WY);KnBY1N+}sf$t514>FS+0MVCO)rajKwW_*-9_*~Yza zAEK>Uas-kZ5G&sBka=x|eSce$1~Dyy(;c~XtY?+n{U%l&P`oqIYvWixT*B>fXpU(N ztKXTm@gF;Th8A46Mmk`ot)&m^BNwZM8`S$897=ktZLRm82RSPj76Z#SB1(`0`UL2e z2%r|rbk7Xx+SP}9_@pg9R~|QV#q}|OQePZD_rvOb=mcqzBJcsp;#qG&$3ox=0sHk8 z_R|nE2fd;-FuPCywb}m@?0Wc*U@&9!yOWQxT+JsyCBZR?@L6w@mzGy~aIY#p&i6P< z9Hm!YW^UJeIOzWOrj%?InqGZn%awRZLVP4*-gU)UOhbzP*&4rY4#>p35P1xiSgq@f z{6{pTEt$EUJX$R9j>MUTU|0Q+?8%F9jJaOU2zga~S? zE~d_7{2@F^NE%#3vR=|t@=(Ve5CyFKUr)>}FL^pS6)2bUmKcknVhP;RU`N&Ye=P9n zZu!^FkuC5YnoO5_r5EN-_G^d%h7Zr&&~UY(pRx${dS2b+Pk$;n&U0ecgnNL@Y&%qLV5>^iFfAq z5^0_TVLuCT#vK#6x|sX9sxoN0x8|!OAQa&ZFNfVml6u^JzES7VyTNoM%sB~{`eoV6 z_wUTD->>h{aSq_P2dD-T+B!S~o>d5VMG!H#fxz`?^*WcFb4Gf4W`Ftdx7IVB_{#da z|ATIL*H_c5BSoJhy`9D3#g5WfCBd@XKIo(}Z$!!O#Kl|6s0S@eD=WFBOp3M(P~1j0 zK}~x+lmoZs7&t4F_|TOX#1En`vHZy2@(iW&UX$RK@@~*B9$~zNyuO9^lhXvN>{R@!IrTWm71J{A~oCPJ|q6~#s3?PBgenC5Cynj9Q zdltI1#>RK-@V9+X&oo?7*$goA=3S#l>7Zcn13ES+^_rT~%=>pfOc~`^%)c_WxPRE7 zK(Q_jxvWY^51{^XITCc1@=Ad+{Ys{r7m#mlrqiJ#lC3nD@qklF4nfD6-K!vzStRTI zoTn=xRZV7*v@dA92w;Gi%UjS!?=&A4BQZ`TRh4xqCA}!Tg14p=U4e~!-TX!luijJ8 z*aMKg{d)B(Wvysg-}$ptYt(jNaPX{EP<)Ne%9wBPLLiEU!P}2Hrg(yeso9kPjQ>Py zpeSJ3tublLP0W1X?P{&$7)EXckQ%>}u+z@P&VDYbx>65r;54l#vPpW+i-`fkXu=C1|G6Ow06?O$mkrhi0TivyzFv-h9@mfR$%7!4&Rk(R$ z(bogpv3*d4*1mVV1n3KY#^^Ck^%XAMGr6q7nQYScKyS?>h%{_IFyW^3h?m`~MmhQO z_sUO@qgUq4OMyv%HWETnTYL32KD)<7(;A=tsm=eix&vtsh(3B6o74AjwCgSA358r2 z7nkFEyxn%(B9moUiAVpse4TC~;J2#;LOvXSDKOv#h%fmyHKqQCJI2bIw^8Yx_(8lzutwtm_f^sE)o#d&2@7 zKcf+t9rSx~D?f@cJrzVck-C~W)P+ZdhqIgO|B~g4#zz;N2)N~? z&82V8gh*waxamvn1b&N)uxu~~~IPziulwNQSqOnylIw|7IVal&R(Gj81^TUGlk>SI` z(UNvYb8LE1n`aS!K)MAAG+6|N;;^I-TuVgUP_(rB=~W#7n`{#^33-}^^p^iTGWR6` zwo}PQRN{X!r*}M}L@$cA+)v zH$|_M82NxAbEHuT`AiqnE7z&f4&3$rG%%vcqK!~(Oj5+tk*wXX`7OJlsc#Z*J5*>b zI&nPxWRB0d4xr_TWPPc!wQwBaaXOKW>;q8o>SfR7vKCGEcN48GI~@i53H7Ort(L>x zi7Py30stQDD)i@kchszFI!gZ`t8B)Vk>sRT#?V)eJM#>^8xEF-rYGI(ny{`c zmEqMGze=|7DB>Y~EG2+ud#8w0|A8TWi&TRym!&jwL-|D15u2G|8)rVa9~YGNEdq74 zQ-lG@~ZrIeeul4zVM8EaK`{H4ci7iD0+!s}H|9Cd+9eZANcR858FJ74oxx z7C{vkE%0_uI_=?Aa%>V5!8fS6#U*w5^mC*B$924L&FdRZ%z(<5l1eO-A-R_{5v_sZJT!ymnLm{w}_hkn^8~kHm@2w^0g#@7fWFGnlGZpo|YR zkYdsYln$lo+3W9GsCD;-inkxF`j}AJZcRsjbw5k7#w_BefBXm(FPwA~$*{r$>|vEA z8VBZWF4y(Q5C``YKRNT^{IkSCM9Yf9js;VhqCfH+yZ!^at^RfS(-?|XOJnv(%t_6I3eHL3%-;;sBoiWQQyDv81(!J66!c&3DPotxKc1E5eX??%M zp8}kG{A4Tdt#iy1Z8d?>N|*OC#YRP#MkTg-Yyb+AOpC)xjs*`l7qzinfcuAL5>+O) z6-=}pxX0hMxPd2BSvyhzh;RU=ndeoFd{CrY0fQK#;2Qc1kg3l~>b7JxB?&XM% zw+YlfuW;*c?4YIhL)OhlctV8i?RBvs1!)o|PU{J5f*hc! z4}w}&CR}(lSU76ruPBy99RDt4^UTs?gm9_vvp(s$r>hq+f^-6-RuuLKbhN=^vh)DF z#wuJ0Aw0Ssc56%h*m%sPX@@Swv48v|6aFUhRrUJ(0?)YQDe~3^J^|@FYH6VPFUExf z$3?mYJ)$0Sm5>>bdauLw>j46!Hf-dlLUNqhuE?s#Om7gB>)_V;Q?VQLi_!h=thhD4 zyGn_Wn+)DpF~T)5X59(!S;K-Fkd2`AS1&S$bDy$v9dSZ4k1DYvuUx0=e)urY(nB@4 z8_1`5|eJTgI+5Nc=LY8 z$8R$zb_CFahy<;_P+}ar#7bcKaib&DMv6}F#C~0NV!nr&NOu^;kVO>-T3EuH`<0-4 zW{)Dqi(A4=$yxYyU$e*i-XuZ|nphbnmj!ELM1G-rhtZsfhmo7U5lfjwrborU^aFZ6 zVP!vJ8Djhoh!17Oc=`7#>^62hI6yVd6XLxIBg+B`7{kNpD?n1|{3ZXOM;2(T#7&2h zC-krRsohwtpe5eYE?}(GO2O453mCpxd_-k&i;kT5p?s6u4f+$y(9;z2Pl*Az`Oq;{ z1X*Ke#U_4t<;B zN%-_#0rkN3MtVEl`)wrqVUti&Ab5r6NPLyCIx~5)C>yYx$i*6`)BgSysN}D{ZB`;7 z9G+Y>W^wG5wpERP49g%YtxMBjK&w3gYfcULD@*azu2Gb$F>9gR^)F7CzB(OMvU>>B9(#;l^Yz8XX`9&~@h}P} zt_0Mm`nUI!-ka~UzFH8gY^t{;tv#egySRte4?cuvffBXn*5zWCNISO`p`@{xMb5a3 zwRRcNLS%mb&#lKB>AO!Mu)i<(rl!4IbNb5oq+6gwY2^Kzxn29+@%p&1hT|^-aFNC_ ztYO|9ecI?G$f9&Bv1^7v_?`Nn!?u+)Wdm>kbJ9B@nzu!sgwN-(EDF)6s6V5i7aI*) zB$7&BAm}8sjg-Gf?tWH)|L;xn$>+aXoWQEM_Z*9YIzcY^|y7Efq(C)+itdF zngu4^!}@mr|7Y5o-;AsWKv#K?l{bW)q%UwI?}s!pfj^0W8T)YvFOAqpnK>P^h}Xv{ zU8gHC64XcSW!>(dQU)(uFD7lzX+2sP{#dog+vW|PP#(I{Em}reYxw=hDo1Q01b}x0 z#(&h>nR&Te*LgLvVE@RCPxM=#Y*tYHYqw@zXw4sCdiK$si=?krWqi=e+tL*4VoVNP zarTLwPA(vtbY#$Dg02!bo$DzlAzBkSfa-b?*KDMjILLP=QP}qK2Jb~DX0Q{OC8$_r z5gT?Tn1_eWsT@qO`}myLV!4B|ca!U@Z&-Un8e{6DMg=_!IB z)~HBb+_3(@b?;m#ALa%D^EJDF2vRYqyxZgb?&npO zn80|rqLepy8+&?btv?pP{hfzAY9i^Ibgv7B?cn>RPeO9{!K2K|xMzK2>j;Em8&oXQ zdLXf|sUFjw!%H+i)e1FpI!4zJu9)YVoeDIN2(cfV)dRg$b;L&IVoj2;%2pnWh{#TZ z646wH@x!2x+7|cSf2vJ~x!F(kod>rCT}G5q+S5t=*Q3PN92!Gf#fmqd|%xQOcKXzhgdZ?WK~P|AWe)k&-E>S8iNOL%4;4OWcG9K zwtQRaC}iBY(CJ$L#6NdDGDqZ@%fBvZnKsWF^PcwZZZ3xSv1jo6uPH5v&?mD?p7ZN5 zr}t=tX75kBl=?y{%YHPhrk!DT^pV#-6LpGW9y4`J*#2BeaxpfNI7(V-|y~S%YP7ZhOaKWfgibotgl~%l=q#afq(4v)Hl1qitbo9; z>E+-X>u5wuo}+|WB27w&Vgv({LKMvQ4hc-(fu@%x5X^Xxj&u2<-mJeDE`tkhaJ^dW zz(c}w+QJ3Qkk|dw`P|^1^h4Z>ad^^RN|*A74b0H`>X#asN^48bT>&rmP?>rn$gw{jCoJ+VUOL`@JFDyComx*x+LE|Z17mwpJp+ZAC zWp%G_rXK+pVrnb*-79i(_O%W{_K~{!F`U&gl!Au|uHlnRM;``av2?0iuLMWFod)Gx zLY~h5o}*lbrJG$cNX>rPf!CQ0B0O*=fNPLz|LVQR^COX+MRir!z%O6dgN$TUL^~2h zITshM>AmNDneQ+XuAk&3i^pDbHZxd~2Nz=2z$2YdX>SQZ%%G)M8*A+pG)IbTE-s;k zGeVRHHS7^jbn@mjFCkwzI4;_e1c1kJG)b_r6O&7F9+vo1Ei< zO1CgI>MUq&NCjWxD?%(@AO89Mv~&w1o|$^08fY8WG*2$}08K@P^X zIUzi6uPvqu?o`2t^EG2>~CRaL`VFng0&7?{>4_pHq_Mp_hLnINfjDZa?72Lqz zTY8Ri2^r#?F?{&Guc>Cf`!x|V!DV>P;mtpaNjSsri6Gs7qRP@jB5syQ3US_l<*=ny zb_$8?GCbrp?mr|P&iVC%FnCP$$6H7uwRlc#4smq5Rt5YF>C)FT{PL`njSU_P!R$lO zZ}Q=DBnvnK-NMJeGHU4Og<%&EgFde-v0u1g=O0tt#y%-Xfy?=O8C48C-z}AVO=+3h zGi5i9LrG;0)wskJcoHij(@V3Dz1hwXsY`8_I#R*fm@kP( zEYZf&yI8T!H+L8xTpZ9@nZvF<9hDZf-qv2r(ia{dPE0%Q_Ld`ez-`$BxU1u6DO{I0 z1!gmJLc0`7YSSnlU_Y%gHo{D{U7qh8uDz0KOkesMk0|8a^2^=kxFv63#Y)L3#Rvhg z(ot)qBIjH|;r)R=zvqtV!EJ5rq$jMc|L1Jpq$~Nk$_QkWoR=Z8pFtV6OX)A;mJ*A5 zNU1f!ATs1&xe}s+$>!s%A06AXXdIX~6?dTtmAIL+l2cP2K}5pk%r1vh1cakcj0P@eonQSXT(wp*UG6`Z&Q{KdfyO<6H8ARz z#?M*Uz%zE92GT^3H&=BiUS`3Uqedw0*O&+X+RE496h2(+1e16aQ^T($YiM~+_Hqh@th!Ju--Bgh59iv*j%o2d0BW(;ivU- zP3B@vCLsm=H-xtF3!MVCA4E8U7(W~&h4lNGVhJkp_iPd45N=%u;`q&bv)-F}#m7+c5;v`QW&Vbbm?L+b z^t6o~(Ne{s;rZ4``%{`{n-DyP$W-%w7coziM#?2E#I2A3{zn}3nhsp!MHDn4r1G%| zgz6lc_0{F8jjz+_lS>S7y(jA|qV9Ty-=D|FkqkkIl@PcA{`xmpNB}Ukuw|=F9Bi9j zh~Lgf#T0fb`{W$F?X!IH#SH!n@BanPqxyPLPYYOTa*i#p(>ADBEYR?2L03YmvGYvg z|DnLCw|@n3I?TPOb+UdJs}*kBnE(>^j{zHO=>-}|6*ex`w=&7{URp@O=i|X>{#8TfZUKt(&2Sn}8axd~OD&w$=*5jva zt$Y@6xEsggHUo0R+rF;rjL^u=Lz_{>f8&JgH9hUvfm>3i5*a*NU6^|@;Zsxu zkwwn=-MZAjK8koiSU;cpB4L2iD6AgF`{#ri|9@w8e?QLWw3bv!*HFke^=uoN%Ekl~ zbiTbyY}7tVcN}WL63B$(IpKJE9jts2FB8IyHIYP)o%8ifBI2<(#LwlUjLdi_Cx}jl zNx$Fxbxm~v?#0Zx{7nC;v?(tky>#xc)b-$RihW4a{kWrN)P5;$)eBhl<6(9o!y@)YnFg|! zm_#giWi?Dh1;=l1x zRhTQJFt{>t3g0c8sX31#Onf$JI%lbQ0B;sln7KGtU^-N5`qLU9*;muOLsw^2KjrIg zThcyfq}rNYVv+UQ39VA61*<}t)isS_mqq($UBLk)UDdnvXsU2iZLy%DEY!IqikxgU zCr;+1B3fH)l5fQ6n`Pe63}#lpCvPB#bZtNXu{?NglcqEO0xGMuI@KAhWU(ZF`>NL7 z8jUvWW&|Hzvg?_s11B-M{Vc{o9gl?>QL;9JkLuR28=>`VYlI=qDZ)ZaqXJdj(lY2w z(BXI9%PF^%uIlAHxSx`18}dnxGVNN=?s63yj)X{whT<31>>G}e@V-C8_Ji~Y*h9~L zu8?c9sx1N)j%e9ld`B^YfWMIUbKh{3$%lDw@)7H(sS+5cB)K5L6@snFgsRc4t(@yb z5ky2Z!P(GIs5+lcTc1?vdl&uCdViJLABH){D^efFXx@xk@YgBLd`L*CbkLZ&lM6Am zG^7PbgfP=cvdf~MJBPzVDsAlkX3V!F^ZoYAT-aKFf4!m|;UJae5NdwJ`m$hnW=Pb; z;&@#FRl?MiBLoju$es{U1RPnrysqwwA%NVCcCra!)%s0)LYWqoGqq&aLn0r6mmE#6 zCIC4G4fQ51C+{v*locPq!i-h+5+y_Q>Q~N5hKpSpn_T&C!xUW_qki>NZf)?=l9Is6f{G^CXhH|61 zii2-Z+RB5^o9tvb(6rnkE+K!ZO`7lL4#L>+jM8Fag1qrJ1mj8kiSDK254Vf~jO*51*Vg^_`6WurV+K?T+}cG&VR zszXo8);Q8*2nAITUQIL{L)S8F9ioaT@t42W2 z-I-b`W~g=PAooGR;>4twgrH718va-&>L9)cQ6jcAHt$S`@Ubv5{05R<@wY=rSM3n2 zZ!ACCj2?M}bIR%aaVIbnZQZj})qj1rF_nv`G^phxWU9a2>zcogf5ax9)eh9GF2N18 zF*?7C_18)NTF-W{^;Ss$KxJ%)c>(QsJIlJWJ?yH#%{hh9!}#DL}L_042Ve9?1L|wu0EMiaX`lxC@aPHHCb@Al1(1e}CI) zudQ7#twyJvcXBH1c z4c;w8Fo&0^ZRTU6MJkx5tEL}769~B&@cS`|P>Z2>`^@9@k z*TZ2bf{sdKSn`X6ReSBwORkGEi{9h=d|3hr;1z9gr}GPq0{6%-Y~C23b50rl!hAY< zVgs+5q@>z1l7%o6kjz#u0u+FP#a_fQlLVN-Exc;`y%fKBVlzBsKVHaNh(~M#AByzz zPP>zt+mY@SsK&*~czOi50DvJ&elR?}S(pN)K9B%kF|zc{1Q;;In-iGBM;znk2hSDW zi|8?T8IkNrUCn6iau8u>JtV5=b;do5X$%#$F-YCmNN+k|2j2srIwmGzR71nkczCF| zG3E8@;FCb0vkUe*K9}T0@@D~pCXtWW%_Uiw13W7^lYe7)R;NWE!j5t;ufKE*VeHF6 zoV0#i<`r&r?YQ5UGfx!Sz(WiL^ntj?fQpkOkx0C zh`|HnJ$s6tVk(bUrkO<%bUNjb`oJJOGHcIKt~sJheDwCo@Ni5b#uzM}0Tzi@>g;W* z`4_fSF)^;?d4Q14Bi)23p^waNhMLmX%twYka|#FSV<2%@;9^D1ZkNZ9L%fW4$2m!% z_bHnJD@Sna4@^@#$vQ@x6|D;ZA+Qf)_;w!F=ym~je|JSnWpb!#%v*@#$D`j87YlX# z7{25A?a1*duvYFcm(V@wjGe635)>TN7D{IY-Od$vZ7snz_SsrVI}dVRF2@HJ#k((7 z9I}ShFNg4zCR>BVD*eFn5gf5Kb1`vKzmy%p0h%|Ey@=`5*-+=P=s79ihAw@K1>}Q3 zBSA|>X4o+Djc zCezIPpjl}77aYsw`^WNuQnDB$pE88olUml>gviQ_NmykyGwO#`!|W}Jnqu*QPKptd z%;!G)LPbT<>6-r(+zXSB%BkUU+>;6-N`PEmi(g_7|5C7L0C5h%LDQJuhbd!lXpgkb+GEH$ zl3W3$r{l|)s*U!hIV;Eb!%n-Ay;_}^R3c_VGAiM02HW-hr z;R{TJu<516Fa|zU^L`vidO5&AUve0asts@B&0&i15wNhsQS6=fbG8+S>R&~Vo;)e; z%Ol~O_-z8-Usqo*DExHHb!<$h@&4Apw;ROYT50kUB7nCdEL1#WqaaTIBajP5hFzD| zv!O!Bp7;Ix;tlb*YrflzqmJ;kRSJ=`uq`i7zQ*F} zzIMuFj_qC>!1lqy+O}!qI$T2O|8wB&f8+GHs=@$e=HPVtB;s9Qn5^5anVo!I11s0Dp(!4?&Ar!tZj@` zv@qeBcR!ImyFE+|v4L-Hx;kZ|E<}j|CwGGbGdM6fOkMwosPdM=P>$KSR%}4Jr1g=d z?0rQ@%Ka-%Vo@OVFd=WV(sF}|Y4$3EyCDG5FKc%31w>I(O;-^%ox)Fh-ViYa@O*CuFB5YG9`YOl^dw95n zF1#QTT9Z*`@cxxtF|j+i?6?;oQQ!9esdEJfQ((=H)BSZKHwMc`A%Kc)bK<;7V3};r zD^rq}&2I;YLvhVh*{QZeMs~~0N@goz5z8~^dIsPU?7AKgpR_=0xo+Cn(26Je@rrBD z1Afazk9gGmNTXAI={oFhAD4pe%&{!PbXr-nQf*H9_;kp-gEvn{5%3Xv>kA;MKhyZF)sv?FoYq%CBAuBpDo?UHB0d#Qk=d-g zNL`(APckj@6EyR1bL72UHEP3{A+S!KnZBbj0}iMWn(RBT8~o@0cs0`p`nNV8AYTSB z$?O@jj#PFzmCb%Ci)L*v+i4GExCQ~eaX?%T^>7&sKb9;Ycwnqjt_*5D*)W6H1 zoZ<*`CZGFWMY&SclE2?3$n(<)L`@!bYy0=3oluc|XUyWWjaiu#9Z1ZMb!m?mI$UH@wk6J*V8iH7Ym~z58AYNRauDC0Dk6?v4*Le~dQkDuO+B^-AuJiQRko zQzuWV?*3V@l@K!>(`9eHZz(F0%CP|rARzOJq(?&~7L*}X3pU2aZ90w3Lhp{0;=fOw zFm*`$!rokPow&u~hq&du`2jiM>;gY(CiYO{rz5(hABntEM~-5@_kHWF;lb(|p+;NC zszTg+_v0WoH@sK^;3P(J?ritU-+ARPp*_V~X_6=xJ2n?DZBecp#TiCoa{ToHc9tW$6SwxIZfammOTzX37DJrW3VL4O&+KtKQd}t7 z*vhD9fX4y=u#UIxrrr4ZR90+>TKsjA`7Yz-1~vJU+qu}$mk*3xqglNl z_kq|CuqRy=j}rxmJss;E<;O~TL@`cTl!<*g|MY<|5DfrYdd(F^y;d%B97{m`Pb2?E zBQCd(TK~Lt0eutF2FgxI!kO2Ihl}n@z1NVjX%V<=v5po5H4_@#LIV zFP3u)Ye8Nk<&C?CJnGkj?E{du3vcov=_Lpw6`khXouyh<8bz77zFhRN>3VnF<*yg2 zgc-jDb*&9~Ac+ry&O&Zm^x62JG& znOqN9e%nb7y*H#vd+@=RO8LvwIbmrtu(<3*Kuw2Ui4oK-IjOt-TgCTVVg@%_usDO zH%V4?wbeQlwFlDoIEK%y$jBU%fZCr^Ui?pa5H@u6s3fDTb@%tEkdlG5OiNSPl6M5F zrloeSmpA#eYz6P;My~itan;?O?^#)a=3&oBpQ+l+Z_Jj+u-p?6NZ$kS#G8-VdMN(( zM~F>Or#X%uvyo+GR7ONRm(XoN-S*gzB{zgEc*94E5Ipj}{`F|~481iapSfHl-2_a~ z%jW2F*XA2~$05@@z#>$0O#wc+3KSz1rrxXvd1jxw3Uy~V7q@Iu+S}_g4tUS8*7Bma zm+hh0Qb~PAnBa=`)@)xd3m~y;*!+N~Sr$+}zv*wDnB2Wc39ah&sgt!+H9B6h3RBCY z?6pxupH55ILgy~SAI%Izep^3xfUvu;yEAB;HUX!|<^8G6>;b34TV$3$daEO?Bz{O(-laDpSqb9C z0Q(Q%lXgU>MQzJ(DOXrMkvtZ4>19A;Q_$8YCxtD~3);mWPAs$82MIVxj1sW_Hr@dO z3S^X)&T?=&-=exb{%1yid-H8ftA$BeNM;|J&6}&{4I``ZL^4o1d#WLjC6JxO2rB=y z#t=|qH~7lh-wF7#qFe%MP4}JA{u4~cXQW%KOiWRmf_wsZok~Ljg5QAbF8d#_uRi@k zFTdTS`3p$+^H*DCi&&4Lj-t9xA-5c#Vq5w`blzwRObmNl+WvgDVtTwpLWu(nzSFpVN4hJ74gdD<}{TJ@vrW zgw#iwk(K=OzW}k)CtLhV^E9n~lWtyDyP2jF`GZA@fjC{^hC<35V8x0v zwsA6@5t+j#V!sp+oyV4bc-c>E4I(i2Ujr{NlavEC+hiN&lJWNBMZtO}q=+p*51sws zL~KkIaR_q*T>!m0tn59g&R5sFzmKWYCt6?o^Q}z}S|s5A92zU}&n6|3jKGNq!e`eF zAr~^3i0n;iT_7L zlHhHRg+%^CDZGll-vn;~y8k+l5JWD0;!U@uRL0JBBO{Edwi+LQ-Eg^S$7>UImsAy- zHBx?Ds4QN=nPK5H`|W9vxx|Md8wWJLj2kiq5(kK|X({`|U&_zY2qVL@SlAfg=d}By zL}sXVl4$sNspwYZ1pAUC1r@J%>v)pV7}Mtf*nwKndtu1=f5)Q-mvAZ$-pOKHn#f#i zsDZqerI~wOeVrp5=QC0+p8mFmRBs0V_-({MMTM0c?fOF#d*y=ZzW=??Ah~lxvIRia zkdAYgN}6~Vndb`kW-+F;;D>#jFS>8i#y=^$YQ&@XlCLgA(k~O%rws#%SJ3W@@(?{s zeV&*neAf2K34|vRYjQ$3Ic?iA=a6q5sbi|IR8D&nW$Gd{zq(v}RdMK0!i4tr`PJz1 zxv1;`?-AtK;uKBiJZQmGcH;86^|)r|wTqiJO9TU3 z!$_Q?-#m4GVB>~2-$M2!9S^ceG<{yJ60;dpi8byQ^0;l6*7|984T0oxV*^jOJkfA{ zBmo5@AStwlmUYUT=C83Y62995f?yjRvYc#McSthb&5Oko(*QhOuie8dIjx%D{8>q? zArlpX;az^2zDK0^oPBL*ZKz!PShNJ0ArQoYo4x=3J>Cn+Qjj`8u<3KPwF%>K zyb6Vwg6t2Xn$6Ghg&S#`Vup%Jny(VdTgj}jh1KDKW3yhPsup?6!nTH!Xg11(J!+=E zIA%7VSXIZyzG(C+8%3dfxm=0~i=9DVAu%+d638=Sq)PCYd%KOSktG+3>-gAMW2I_e zb;Qn0^>+f!6}>$f0#DEBPJ7m~*VrxuQ?uO_PI2h7_Su#PT|I?UAen0G=QKkJC?m{J zdTSVq`D~{P-Csg7yvxPd{%E0_?A?kz%*z|!J0sm6YUW=4(^M%x;mYrN|6NS2NtLuZ z%M^?om;!ze^6Q$l3MRjC{maRVv30xi!&E1W$Ypdzjo545{(hO< zDv!$~t29kJGnWlX^4=^GpoU@&g6Irs&I4O*6J6axWH6g*aZic)3I46DZ_IDszRYgM zq&a8{*@nsqZ~nS!Bt_8(yTV8dB0wsmIlG87?yXrWW!K3R!z@Wqv8=Z?z5DTnY&3dz zAvfcVHL)~J&R2pI$-8)>czMJ@1m`i>-I*`Vs+B6>ZgT)73`C-M5QXx5C7GF%(_LZu zU?Y#*yi-w8s&(IpJqjb``U&4t-Aap7A}E3;1v;!wtXis_a>RzqjRM>%MyaV;d(vH< zzBXt3+^>sISn2Y>*mi|{YlAOeZ6u@XLB>l$MZTu@78uJZ5lZg@0Sa_Xz(5K}h&7cT zC)-eWcgE4woPlt}fzn+zd3Om8ubh-kdm3fYZ05I%iGgk-a_*|u1HSJA2=tKb(Z0Fv z>%)!T`@{}Cd*!)N7hiCFulTvMJC-5CA4f-`s#r!-6HRfl<`X zEbWGJWF?%J;GZO4}V<^_v_xYN3g^xSbS~a*bfm(WYA2U3_0JWWvb5Z_l@1{JoIi-cD#yG!1jk)`)1+3t8c+f9cmFKy$f5S zpLskoar_RLxl`UUoP)%8i}e-sNqIyu7)4pnC3fFL?hdv{=S?)U_}l0au;^_%dbzg% z6)}+ApO<^>P@zVWLt1OG?vbRY=R+~H8%#eBvlK2!Gsi#!@ z6G(qr(b}YO+Y;l(tf**hk%_eH?YZ-PYcT9J=;W0dp70isg44{}JGq&Qnho6q0pIXAU#f6t!w3$Z^@Cg5I#T&r?mBlqbVf7$Ceovm75Dz(eK}08XDss@Yok>2#*p zZ-2&=NhE!7ZkUSDH^U5FI#oP7Ks0y975^?f@k-g1+=5O!zEX&AFs$tpEtN$D zT841MKK=g|Snywm_AlYdHjZfFB&9yho1hc$-_={-cHJ;P>hw?V_SIIzXhG)qU;Lv8 zBDiB@rU&;!DiswkdPhcXe|9?#s@RY9ehMvNx1Go)438!GN%G&;2c*(ktvqUd!tMA9 zy79W<{oBQvP*Q7^2^a$OkzOWa>h>PB=5pH0k*36t+PHO8*0W2RGU%vQ4%g1Pe1K?e z=4)*^36sxY%BIA~zJOt3-4`TPUDX%gRi$GXNY6N?+LKX#@}u>E#0()NdmLOcMmpFG-Hc))po>JmVmb z*mUZ-#Kl?ahs^hds^gCM!bOyJDIcWtz4Sha8wpqV>f0hFCzexF@dsSnDkGE;?%sAB zN0Yok{`pIsBk`D(VNNS3-0G>K)7dpFwa348_Z!@MQwF$VR?`K`gl2@!T#^hR4NN58XMs6hRCqg(9 zA3QCbCnn*n{k5ghKq%w}F89W%bN_=iRg~fG4wu+Y6`>B`+dgo@&aioZQAt;4Ui9wt z%l2YR?f04jo`@1wnB)0fxm7s|nC^7D`B$p6&2yqb-jPi$QEQ?^)AJ7(W#pVNL_$bP zETM)SXfE{i;$MZ(qOdAsUDDCJP21E`6iWF`0K($#msz+zFZ5LXt^=#Ak_CF^Yn-rJ zmNaz_W;pvU={9`i_ezeVg@D|U3zHhND1-G#-)RadWY|Ipt0LTPt$&Ulg4l^Q{IHXC zMY|l>>TS`oz!$n?17Mr7*cChBR#lvdV!FdLiicTX1Z53SQsCheCb01-dAbZ(S+kUutr*VuSv|^=M(e5b-{fBV5g~8ImBSbw>3pF?Q=w)!$Vv@)I9igk%l;TiV z{;VH`M)<7$YWHu|cBni5p)WXxD?!ba?F?qa##uT@UW#FJ*`Oc7Y()+C2o+ocjrF@BaFol-i>{jkRuyB7K#^GL+@m z=NJywgl(=|d3jRQ`l@q8o{QvTM1?!%euBnl6oDU}oPb(hN1CYA7H0C9ig27;aqCX{ z_Vk9Py;cxN4J7z&z7O|`_1D!M*L9RVi-cFWtE3lk7*Cn94_;@yg_JBA24l0h2d3;v z2W6Sc`3JB^NH>SSn|}E;abg?k`DRZudb6*(J5BzSXd;YI8W~wn9lLr-U*(hRnaomMyM@&Nv(BVvEwMvvFG7*Gaz;%dF@Q>%TPI2 z9p;POL=S5`Zc&6XoHb|7?E?mAZWk??1+X@qeA7E>lvN8EEJ40HWGeJ?Wa}jot=<^y({r-z|i{fnl%Unz8 z#>6*-f`a!+f6>I?7e^vWJAE~cR&?*S6@oSrW{Y|?MSUdRR=T_^0{gM8QcfR z`CBETm#3tz?K{8hG8uj=;5=O{F<2aQr`u1;IaZrKpllnd2vxuACv*`+J?pX@q<6st z2DdCH+fOtfkG$UcB=Q$c+SAWsG8p}b)gj-T@Iwv>K-$%69Bom@PPVZs|Au>5w|j_0 zeg%9V!x7esCQ=Ti3!hBkHys0vmK3s0qbX$O!{B9yNDBk*Ma9Q}dT_3jM8aeD*(pNj zr|+%$6N>%(DYuE`oKHX$G}#~mp&xe{l>Ca9t|uLf*?QVVixA5xEA!W?2OM zz{dE7JRZj#l;4E76^~k0#)0%SU?mr+(zeBc>Y}be`Cizkwqo7rjcKh|_-Iqm_7kG~ zZSGY|4Zv$3LBzkJ7A%L|<0=88Epv&e&= zZvFFnlnoks$vLIb-OR*eNHsC%t3-Osu6nC2<* z1Fe@L9!? zRsu(_X&}WvKuS5Fmw09}+B=f2St-VtEUpc6j(yn&yNV(rvB%5@yMuN8SyB~+hz`)$ zIR-kp4rtCdCTaP6&49s&DDc@wLOov(KL#(X3-aD}gOXZGUn&ypnO=vUHEWp4`zD>s zzHvUTlkE6(?2de(VTd}BZ@QPG!5}f4?y_+X$p1rXt*~GE?vIY#b%dYpEFPFAE^L4} zBuc-ji|B+@Pv1FIrij4n__3tQxTQIuvGA%tFSC~F;N7NV#5&d2d-`j@{nn#b14An_0QrvfKTxE~;Xt z4w0K2eqQPHOJy%WY@Gu~R;0$LBfl=4ex&hPi6CkY4ltbDZIE#*UuJJs5;*Ybp5WT6 zdB^3d%zOtOqfGpkwJ$Y17|9CDV48rr$qQkHb~#MhcFON_`@}fesCwE{uVq$=nLp!F zOYNp|u!9X9;|tMVqbW_=#p z)(Irb4Wdg52^h+sh%_)~7m-~^B@&tt0t&92=$P?o>w>NQDHkQ7fyM37*d}V0z;D{5 z7PTJ;85OwXpGLq3s;Zozg5kJXc#pmx3*^t(;bE%bvgr&;fEawxt(>Dx!Sia@`sS42 z@U_mI@okPVD<}af;kxc*W5`YFbh?Hhad|gC~v6?LAhZbc?Et@L?W05xG zJ1hZwAyobV0gMc;{3$5lw;_}rvh7-cYZN6^{dZsIznu}HO%~Ma4pF;1x;on*dfxr* zrrY}WiRK_h96Wa#TRYABs|#U(ibM9fPHcn(es5J>RNSOhAoBAR^$Gg{zL3p5N`t%v?TSzpI)J zfB%T%V5P~@*D}wjy`z#N7e+aiDAsa(d^~=0<;z$T^vJb%Ar<}csG9`bH3#(NgIm>2 z18L>FgBw&>EHQMi*nhDU{+g!xfl`~X@^dO|dyp=1rK-Gn!%icNIoh#Z?G4Y>oGi+G zOS2*rqf}IoHV+!qb9ZF_Miz|DpqS?YeEKnvSx&daeG@C6b{9*ls~;!t{QfqARVYhs zP4xQ!5r_31%RJq5~q~(sq#e8i$2wPIJ0Q5V`n3-j~5IdrL02 z$9!OznrF7n+B$fF^1I`@v=7QO_p)1e=J2yEJeY5w7c5|YUdi=l{|?)fpzET;X%u3t zuabLNYQ}NE>zjx*PrcD3|1vwbV70sYG2S&xWovyUhprLE=L|opC4l$kBdvCE@cC$X{s2fc8_~a2IY{0A z>9M}wxt=(krPgTQ&FMjvnu)1~sO`?bBSWfFm4yb5=%~-b)RX4MxC^eJRH^sQuG`hT z3Y~hO4e;>hN3|n<)&3`=H>amkz=T>|^!+;Y`?9u;jg4^lh_R_R87m4HE zM30+mTRJQAR&U&)&3lpI3THjV3bQ}Hsx=K3$mg`E0xnVyJBE%)p_v{2zcE5?!Erd*|R6D)i#a*<$&$T z&wW3&wLua#LuSnhx+ZBW)7<$=fjHjA#{eTT92vExhdHpO@Cf0{BKcI~oS6;7Dk`SC zlXp00a=GEM(dPaiou7;Q#V1%(iU0QHtT?p>dw@o08Qz}h_UzTvN%8QUy(f){=}!oG z;$(_?b<2ykOkU{k&%tW}`-C9*v&Uoj7Av9x7dF`K->SA2v#zW4YD_fVosn5Ezv}|V zvRiFwk(k9_uanpCtDYl2B))ek+}k&&8P0k+Ozkqfd8;er(*iPFyjp~_ot@dSz?<8u zipt)*TTe~?CKsNk7tHBY^7dEAS%Bcg;PG-WdAY=!E#h{(m|>f#J=fxb9Jq8?C7;1QeDdmcJr zz+?JwT?v_n&PzI!xFA9`$3(s6QUjQNipsQIwk~_8VqC2vCnAgnSYuwGvkrNYghxm* zEW4`#F^U%*45L=iUkHPFF0p-3?|MBxqN04eINh408tk)qAKCAEiba7yR9{`mc=eQB z_-*K;chvTe-z&CMo!01pfHw>*Ihr=G@ns4W%>K^gRtckhZ}bKl<<(Xdr7)hB=~?c} z$-(?a-CB8dw@jXk&T{<%(}|!9VhXG2qa|I`>1qGwzV>~t{rlEa+ws*U6To^dEcSh& zbf-Ok=1_sT7;5vq(D)#^7+$*2Lt4pFj*Ci3kN>7*14i~Fw7kR8`dwlN__;72=OC5e z?&$IylQ19t{Ap>UcS$~;Dwg}>sQG0f{i(QBm!BSE{>lUXF2=WN#jZm43|N+by9bNt z%bXfioYN%NhooBXiu6d|wFioO3N;woK9$PrjYg zYv6UEv@pQH4wEkmpSa7KuKddIZ7ZgLIcDy*xGv!ULg~!`IoK#gxM8b(y|Za(la91y z&_~xZEOWk=bOrmTnu@=(S#AFbJ?ar|+#@I$e-}18wm8wyZugTNNzVksCwVQ8 zEblNbN6Gzrvjvgs#7UzK#l)+ILLyvCUl0UuHv_2uxW=t;2{_SLy>P~(5=}*@27O2* zGtx@20Xf2ZQ77GUE@;u_d{kC6^_+yrg^6DQbyacdDHZ_PLp7NW;9C))!1G#hSAO*IGTk|Y(-759oe8|l zEi+G88dxLE_okg%j#R3%f^%8$aALI$^K4QP7`rY=G8|xwZF4-U)?G(VjQc+F2FfmJTlZWgbgUq5 zvFgrl=KfP(J(&3CHL>p1KO%Kt2Y*#bG}~lg1&LSo7|>?5CrgooUQ)G#G_vpi9#Mq; zs_=&dpV;lQNz%5aYe@$w@H>k3@Uz?Krs2~56GSeGMx%;?bT~8WX5dI-->DzCvT|;Y zjE`#MO1g>BAMIAn_`V&>GT$`rvTs@Ue2dHxmkQ{k(K%n&y1!2jJ`n62;$)We`ewj3 zABbOWdpQve{LI_V|`zZPJ6Nv z*Ov+Yvjpz89tM2PY`LucO=J4zJl8#?oE=&4Rl?@Nw5&JxO6e1^d@s;XruwoP8Mecv zaVNI(8g#NbO!n=*jb)<={%0or5+SB5Jm#|T6om6?`==SjEhmB)OUA;KhtKAet54Iq zo_hU(AOE~a@An+^4T=DkncmRV<;ENP&)mk;Cjs7oeGc|8sx0_wVNE1lA}88>j@SE& zr9p#vUHQ_#KEffcmD$}`#%3^ZK!qS{{ZqN~-*@7&4$>wHqYkQTol-m>o0@pvJVcW* z(g=Hg4iaQJvKst#F|vi=vFJZUQFhXr;;_3)=Y4~Q?c-i1LM<+ovr(Yc9>Lx2HJ)0(&pnw=q9KrAtXKe95^V#4CEguL+b4+H8RD;mjS!L&%~0%vS1wD(mvUyd^Bv?N8mA$DAeDlsYYZ)@-J$EfWlY ztRjSvTK<&CkHafj{X4Lb#JZ4t> z69MW1r*2kCzAiUQOPk0~k`Xk#5h|~6X?0wp9qrK}36x2gKC*5OEbD13>Y~d|lm^mc+Ak zu5jU{+9}ObQ=Y^Zv5Te-XwD0dvi`Z)_y4tU-SJd^|Nm`&NJHskL`c?ER@uA6wa0al z5T8p{N%o8=N;0}uGRnBuy!I4e#JLQ7*llmiZv_9WCZYsjP2UL!$aX;FwBNW3laJOgifS z)>*6i&iW|7^YBAW02E*mmOG{iAZ>&e>yt~R(?QCOZ*n;3B}4iTl-}v$LL=T{W1`^% z?nDFVedGF zkMaO^QEw(C@zkwz$_Vvf#C#ZZojpG`_ariyH1_pqcp*`u>_0=qa)VE@-?mD?1MH#a zgnMWZ{Jo8xG80HP_Oq2KV19~mpz#bXQ_D59`=prANfmc(Gb$JQFPmn^=Cz$95){9A zkD)-4JvNS>6Mn-Oqc)T?8+UeurpI=C967Wvd+b%V8-XPQyhNBseAmSOpOz*9UjjnLt^+%8+~-*TQ5EHewM`Wpu6a|7e^ZFfx`3JDVMr9 zxn%<3WW$i(`XDzmqPXpZtn@4Vp)9>Iq$Q3A=V0A8fy9AeUouLfC4xqG(RI)yt9oo2%t zcNIk>e9Ftlx?g-5y$bGt7s(#fJ?m0Se=!?yLZ*=Um);sd3{;9)F(~g5{9#W|l8#K{VHY>SL8zt=@l*ADA=Ca>v9JM(Td;GnX2F)dJZA5e}rWO1r=laGl_kN zTP|E*kEqVD^Uc2bgR@*ZpT;mX>~|fbH<3u7y?+4{io5d*UT|{5w&03a=TN@d#fv&@ z)m%Ht#2yhf^!~G-lA&*AlVIeWJ?Q+>#3I z({VeE1#&t~G2UKFa?zb9Gwg{3I{kFuPb=z79+@2M$53wH*!xxPOoV5PEtoG;D?G2F z&e#3{1)++M#cb}mbib6G6SVrM#gub^i3{(h99@M%D?575W;`OrCD`pefwf~GM1CUD zD0uA*H)LjBu=kOE7&eB>&Q$*HjA15lnopKRnnUSZl40Jv%NyF3K!2Iflh5(@S37pL zV2~`3*}W(evRE#n=~X|eB>>ZJasBNbJJjOdk!At`n9LHoD-%vVWo2^;^;#x2fEzu+ zALnvncF#oujmF|%1rl0?U^nS?eOM#mii@$mHAUnu3FS93I=w4P)^fxMyNoROa-nhpB_;Q&`)fz`^DPR z6n_`xcdE0P)O2lJ7P-UQ`t81C3?z4@uu_&X*O5$LGR~K_*$IglcO(r*!uKY^gcZw-CN(Eh~*i z1XeXAx0zctxqW^9`==8(M66odM79HM1DqP*B=5T0sK+?o5?4S_=t=2epte28sMxd* zG3@J*0hoEM(xjdi2Ss=!5~UfR9vt4mJa5VLc>`px4-orf3{>t{YloU51_>3U+5Fgoz$Sz4wOKmW>tF8WPA7mkkKIfk5 zL%k0G)r<0mph6dWz|Olo;~e=bQRiRvo4KKdHlXhn&9&1$de`G)D9dFUU^)7-1Pqyf z+v#k(tkUlo?7EGRHe2k-0S!}`=x>0vXk)GIyFo;xlXWSB53b(C9w=!~cbB2g^lD2+ zKdT}&owkJ>_;!{y^SN!-c6PIx&*W+_Dmd&9`U`@EzHkkXsA@w7r5Pde8;)A_fGvq_ z)Tu>ZZg7bq!QCS*frIUuKxaNP)2G&xwx$&6&9wLaTfC6m_@Xn>MtAvqSkP!L_8b#s zeh`d_S~%;^N7&)9cI#0ukq1v?wyZ_9>(eJi?#jzFRI4!#(+w0!*To1F61vZ#ntBID6qP)9eZg^KTdv7x=no())uZf6BP z`J@GQ?Qo7VzQB9Ya3$5m@8+710HpWeKa-Mx9};g|sVbnX`YPq!QCn5G?|alNj*zJ$ zDc4Z^xDKDbAOk3N)Cv*0tl0jt-dF6LWwt0)07`{Mr5okYxCSSWE?&&Z7i}9ZP|jh_ zaE&5Fs@gj zlmg2At%BZcc*lsXdxh(j-hv!q@KTrDjVn# z>rvzl{1#|E!rYu&KV})_MJD9=2-DV0VI@9JTkxLtF?cx`M!h{R0~fBa3D3i+D0FHZ za=J>>kKwVlS`Tj3wZpwfZ6+wHc0Hq~35sn@m%W>+UeepVq-D^P3#u=D9NBY!vlvO} zwc$~LY(VoN?!6h@$S~5oqHU} zH*bJW8P*Y7>tJVM(L@>Ajvmwt6O7?w!M0@|_ZQej%wXbH!#3FCfxfGW-(bA+M$1W4 zjnI{}hdA73C^L}eYJ|o@vl6dmm&E*ihqEjY|3f?U+(hm5&9+*=^Vah_u9^zBnmX{@ zSSER`2-6hEs)oIIfPV%Jw06$KvXIV1fI(gh9oT;<2ajX&=6h^Ese7j*%zay?Jsq_7 zi)QnA|JfWA0dzcz$$Eo|b!M#8g9U$n8xF?F*p~ZX7*FWxE zcrgNt373O*L~n<$8HKD0XRm#WNiDTT%F&Gx@81Z9?=M+Yq>5LhJcRe_>gy{sRz5ge zhtKy`kK_W7@_^03=Phkd5oi>tKmLfvsDX1}n+U)nL58;U540%uOex~mbpHj=n*jO$ z3~(?)Xo7o*_4V<01gFDqT(ID&pp^ei zn&mNn3M-VO)?{~yP?_-6AJNGE{_+Zs1uU`RH4F-hi2phmsAD}5sR`UN$ZXPI2YuUL zrlj}molhc2z0%wW4fuNx4jHMbhn}3rx2H2N10nmut&eU#({95d4Rc5J@FZ97UW3Qo zal*I{Yn6L+V(t58Lu82H0*C1Bqh4=oHzWE*nLyV0Dr~gW=lpZGpASxH;w<_Lk6JK6 zfudt|X_YsgU$qn;0kyXnI9)J5yTL3@YI1V!JdHhq`Fk{b$lq10BPwT<^F{;Y-GmRn zgahvPRV`^hHUhOMJ|Ni&i_8vo(K$I_vd++sFmZc#RWbpNc=9e5y!Q+m6X;ZQw|O$P zC=ULpa5G?Xoe}?mnAF*!WoG=IFsclG{lS!1nOT#kEsS;vviQjhReNvb5$pdfT(Xk< z(8X&H7^PePZ;9x-2;nZVS6P`^357o{MMH{V5<*vuy z`a3(SZae6@qUDTg!X#MYK`Hy!QJ*9%8^%VbGi;s;H12~SI&CcnDvi3UFq! zkG!%Z@BKaoPkP4*5?XF5#`1tt^)4?UZc5La*c!355AK?`;WMA)K%>rpVNs$K2zljE z@m-Bv3#os5gR|&O5ES(f3D-+L7yY2GVRa2KvUxQ8T_Fp_C+C@`LJ_}4`ydJcdOj)a z*!L6cD8z+}RL{ZDX=2&m&?ZWB-f6d0DnG@Th#Q(R-2rp2r<)S~5`CEgqOU0pD?=pk z>|15%p46xT&0aiaVVA5V#%B9P27*|EI zs=K4^A@?d9>tELhT7lqVAeSSX(Q)`)d;cYgD>IV)uG>Ulh$`bI{^a$PW1q@$I zgDy)=fT#zu&@YhfO;iDrJ-peT4jZsmD(Kh)#extS{*VYmvgWO-Kps*HywH0SA8hr> zdAh<+w3;3PrUR$boaLmgKOvpJn3^B6D9RbA}Zdv@C#$ik09YZ+Dv z;v=lX*5M+-tAzSrfMdd&3-CAwz4P-}s0zv~eOi18ibFwlLc;%m zqn3_$YAoK^#jW;@E=CAOr>!oP>BHg7sw0V$ zaCHw*#DEk46nF|7^T<1T;C-E2Lf7TjaAIica~L^!W{;k;R&fsrjFGG1?Cwdk%Cg?! zt9Zh&0RGCZ&XNn9x4M~LxF?WoAn#Ih?ncBL6L@IBMuhxwi}S!$Q-{pN{n30s**I3) zm1EFn)}8P547Wc?K)Pr4WQ;7Cpd8hv;cdPsXEbK^q}~KA?_^;_pLW(h$9|0Qu|b~I T$H{;n1A>e%Sr}AaaE<#P*f#cX literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_icon.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_icon.imageset/Contents.json new file mode 100644 index 000000000..188a203cc --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_icon.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "Subtract.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Subtract@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Subtract@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_icon.imageset/Subtract.png b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_icon.imageset/Subtract.png new file mode 100644 index 0000000000000000000000000000000000000000..16be3b51b7ff4ba41342ea8348c53cd12ce462b0 GIT binary patch literal 1111 zcmV-d1gQIoP)lv%n2Yg?Ipt)5Hn;R zG10((go`w7jaSi&D*z|-AU_s>-vi((iWz$OXF3%k*{$EfV&JCRgqIBd7`{OesGEO+ z(?uS^1>gy7`o8>3U_FxgPn6bqgXY2CZm$_!#SH3SzTLk00%yV3Zuw1_^8`lVO##2U zL-3eHtBQ$m1R8Mn0IrghSSIFy()ItsJ&Jf?OOL`lAbEZgy~{k(mIwdGts12GFz~tl z>7HA`RG;VPGmBbV6x%8sNs*}p=_K80WesfnxKN?zSN;du>g=|k>EHhDlLA01Vx3GT zNxIkWdEm2Zi3C%zv48pQNFH97>zOKxjwF*!LxL$mM(A~Ro;Hqn6haB}QUC~|( zJopZp#M7fz(j;fl2@Bv~8N2F8)Em3)BS9Sm0zjuR;PB0rS)$MBZ$A&JZ$0}n-K*XL z4erg!k$N`Ph0<4qAUqBx@kiy<6%^#VP*jn~Ih+@b?J~ShcUKlaw zy7tWOpdglC1Ogtn#Q~TE0+Ytbs+BYy&-~uj%3zrj#?Y8BHewnFO}7F2CE-krND79S zg>Pkz;tprUAt+|^h9meX-VogVw0*AbwM-xvVnP&#a+`Dhs3e;~Rb!CjKy5anhoa{< zge`kHelp~eEqO-I+~ zz_phU26CDOMz{=6B-gWNEP}Bx4m0uy?}K$nmhU|AmTD5^uZNfKx)NnGZB*B*Sg7P? zFHv2#wTpS_yXo-rlF`uY9^OU$)9fhB!?O_Ie0ED+E#EQ3RO$0V{6 z1Lg=A2|Ef)nGEg%Ty$5iucZEB@F dX=}CX`4=bB_gt#VeJ20_002ovPDHLkV1g5G2^IhV literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_icon.imageset/Subtract@2x.png b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_icon.imageset/Subtract@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4e20a1517fd39448ff15e283ac19d8f70dcbba65 GIT binary patch literal 2229 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 literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_loading_image.imageset/Group 1@2x.png b/Riot/Assets/Images.xcassets/Room/Location/location_live_cell_loading_image.imageset/Group 1@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f8054f3d447b248b4b6445cf8aa315c7f4fc9ca4 GIT binary patch literal 12255 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

    !; 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 QRPB(BrNg=BXFYHKAFv(i^#A|> literal 0 HcmV?d00001 diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 83f855569..9c0a6f89a 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2145,6 +2145,8 @@ Tap the + to start adding people."; "location_sharing_live_map_callout_title" = "Share location"; "location_sharing_live_timer_outgoing" = "%@ left"; "location_sharing_live_timer_incoming" = "Live until %@"; +"location_sharing_live_loading" = "Loading Live location..."; +"location_sharing_live_error" = "Live location error"; // MARK: - MatrixKit diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index d9c9dadb8..6e39159cc 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -174,6 +174,9 @@ internal class Asset: NSObject { internal static let voiceCallHangupIcon = ImageAsset(name: "voice_call_hangup_icon") internal static let liveLocationIcon = ImageAsset(name: "live_location_icon") internal static let locationCenterMapIcon = ImageAsset(name: "location_center_map_icon") + internal static let locationLiveCellEndedImage = ImageAsset(name: "location_live_cell_ended_image") + internal static let locationLiveCellIcon = ImageAsset(name: "location_live_cell_icon") + internal static let locationLiveCellLoadingImage = ImageAsset(name: "location_live_cell_loading_image") internal static let locationLiveIcon = ImageAsset(name: "location_live_icon") internal static let locationMarkerIcon = ImageAsset(name: "location_marker_icon") internal static let locationPinIcon = ImageAsset(name: "location_pin_icon") diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 13aecc0c7..2c5e2c007 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2763,6 +2763,14 @@ public class VectorL10n: NSObject { public static var locationSharingInvalidAuthorizationSettings: String { return VectorL10n.tr("Vector", "location_sharing_invalid_authorization_settings") } + /// Live location error + public static var locationSharingLiveError: String { + return VectorL10n.tr("Vector", "location_sharing_live_error") + } + /// Loading Live location... + public static var locationSharingLiveLoading: String { + return VectorL10n.tr("Vector", "location_sharing_live_loading") + } /// Share location public static var locationSharingLiveMapCalloutTitle: String { return VectorL10n.tr("Vector", "location_sharing_live_map_callout_title") diff --git a/Riot/Managers/Theme/Theme.swift b/Riot/Managers/Theme/Theme.swift index 93d9cb2a9..039175371 100644 --- a/Riot/Managers/Theme/Theme.swift +++ b/Riot/Managers/Theme/Theme.swift @@ -104,6 +104,16 @@ import DesignKit var roomCellOutgoingBubbleBackgroundColor: UIColor { get } + // Localisation Cells + + var roomCellLocalisationTextColor: UIColor { get } + + var roomCellLocalisationStartedColor: UIColor { get } + + var roomCellLocalisationEndedColor: UIColor { get } + + var roomCellLocalisationErrorColor: UIColor { get } + // MARK: - Customisation methods diff --git a/Riot/Managers/Theme/Themes/DarkTheme.swift b/Riot/Managers/Theme/Themes/DarkTheme.swift index d61ed3954..6d076e037 100644 --- a/Riot/Managers/Theme/Themes/DarkTheme.swift +++ b/Riot/Managers/Theme/Themes/DarkTheme.swift @@ -98,6 +98,14 @@ class DarkTheme: NSObject, Theme { } var roomCellOutgoingBubbleBackgroundColor: UIColor = UIColor(rgb: 0x133A34) + + var roomCellLocalisationTextColor: UIColor = UIColor(rgb: 0x17191C) + + var roomCellLocalisationStartedColor: UIColor = UIColor(rgb: 0x5C56F5) + + var roomCellLocalisationEndedColor: UIColor = UIColor(rgb: 0xC1C6CD) + + var roomCellLocalisationErrorColor: UIColor = UIColor(rgb: 0xFF5B55) func applyStyle(onTabBar tabBar: UITabBar) { tabBar.unselectedItemTintColor = self.tabBarUnselectedItemTintColor diff --git a/Riot/Managers/Theme/Themes/DefaultTheme.swift b/Riot/Managers/Theme/Themes/DefaultTheme.swift index e2afd9339..5eaee1185 100644 --- a/Riot/Managers/Theme/Themes/DefaultTheme.swift +++ b/Riot/Managers/Theme/Themes/DefaultTheme.swift @@ -105,6 +105,14 @@ class DefaultTheme: NSObject, Theme { var roomCellOutgoingBubbleBackgroundColor: UIColor = UIColor(rgb: 0xE7F8F3) + var roomCellLocalisationTextColor: UIColor = UIColor(rgb: 0x17191C) + + var roomCellLocalisationStartedColor: UIColor = UIColor(rgb: 0x5C56F5) + + var roomCellLocalisationEndedColor: UIColor = UIColor(rgb: 0xC1C6CD) + + var roomCellLocalisationErrorColor: UIColor = UIColor(rgb: 0xFF5B55) + func applyStyle(onTabBar tabBar: UITabBar) { tabBar.unselectedItemTintColor = self.tabBarUnselectedItemTintColor tabBar.tintColor = self.tintColor diff --git a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift index 4eb372b2d..c500bf87d 100644 --- a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift +++ b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift @@ -19,26 +19,60 @@ import Reusable import Mapbox import SwiftUI -struct LiveLocationParameter { - let bannerImage: UIImage - let bannerTitle: String - let timer: String? - let shouldShowStopButton: Bool - let isLive: Bool +protocol RoomTimelineLocationViewDelegate: AnyObject { + func didTapStopButton() + func didTapRetryButton() } -enum LiveLocationState { - case incomingLive(String?) - case outgoingLive(String?) +struct RoomTimelineLocationViewData { + let location: CLLocationCoordinate2D + let userAvatarData: AvatarViewData? + let mapStyleURL: URL +} + +struct LiveLocationBannerViewData { + let placeholderImage: UIImage? + let iconTint: UIColor + let title: String + let titleColor: UIColor + let timeLeftString: String? + let rightButtonTitle: String? + let rightButtonTag: RightButtonTag - func values() -> LiveLocationParameter { - switch self { - case .incomingLive(let timerString): - return LiveLocationParameter(bannerImage: Asset.Images.locationLiveIcon.image, bannerTitle: VectorL10n.liveLocationSharingBannerTitle, timer: timerString, shouldShowStopButton: false, isLive: true) - case .outgoingLive(let timerString): - return LiveLocationParameter(bannerImage: Asset.Images.locationLiveIcon.image, bannerTitle: VectorL10n.liveLocationSharingBannerTitle, timer: timerString, shouldShowStopButton: true, isLive: true) - } + var showTimer: Bool { + return timeLeftString != nil } + + var showRightButton: Bool { + return rightButtonTitle != nil + } + + var showPlaceholderImage: Bool { + return placeholderImage != nil + } +} + +enum TimelineLiveLocationViewState { + case incoming(_ status: IncomingLiveLocationSharingStatus) // live location started by other users + case outgoing(_ status: OutgoingLiveLocationSharingStatus) // live location started from current user +} + + +enum OutgoingLiveLocationSharingStatus { + case starting + case started(_ timeleft: TimeInterval) + case failure + case stopped +} + +enum IncomingLiveLocationSharingStatus { + case started(_ timeleft: TimeInterval) + case stopped +} + +enum RightButtonTag: Int { + case stopSharing = 0 + case retrySharing } class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegate { @@ -61,11 +95,12 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat @IBOutlet private var attributionLabel: UILabel! // MARK: - Live Location + @IBOutlet private var placeholderImage: UIImageView! @IBOutlet private var liveLocationContainerView: UIView! @IBOutlet private var liveLocationImageView: UIImageView! @IBOutlet private var liveLocationStatusLabel: UILabel! @IBOutlet private var liveLocationTimerLabel: UILabel! - @IBOutlet private var stopSharingButton: UIButton! + @IBOutlet private var rightButton: UIButton! @@ -73,6 +108,22 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat private var annotationView: LocationMarkerView? private static var usernameColorGenerator = UserNameColorGenerator() + private lazy var incomingTimerFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "HH:mm" + return dateFormatter + }() + + private lazy var outgoingTimerFormatter: DateComponentsFormatter = { + let formatter = DateComponentsFormatter() + formatter.zeroFormattingBehavior = .dropAll + formatter.allowedUnits = [.hour, .minute, .second] + formatter.unitsStyle = .brief + return formatter + }() + + weak var delegate: RoomTimelineLocationViewDelegate? + // MARK: Public var locationDescription: String? { @@ -84,7 +135,7 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat descriptionContainerView.isHidden = (newValue?.count ?? 0 == 0) } } - + override func awakeFromNib() { super.awakeFromNib() @@ -104,12 +155,12 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat layer.cornerRadius = Constants.cellCornerRadius } - // MARK: - Public + // MARK: - Private - public func displayLocation(_ location: CLLocationCoordinate2D, - userAvatarData: AvatarViewData? = nil, - mapStyleURL: URL, - liveLocationState: LiveLocationState? = nil) { + private func displayLocation(_ location: CLLocationCoordinate2D, + userAvatarData: AvatarViewData? = nil, + mapStyleURL: URL, + bannerViewData: LiveLocationBannerViewData? = nil) { mapView.styleURL = mapStyleURL @@ -131,23 +182,116 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat mapView.addAnnotation(pointAnnotation) // Configure live location banner - guard let liveLocationParameters = liveLocationState?.values() else { + guard let bannerViewData = bannerViewData else { liveLocationContainerView.isHidden = true return } liveLocationContainerView.isHidden = false - liveLocationImageView.image = liveLocationParameters.bannerImage - liveLocationStatusLabel.text = liveLocationParameters.bannerTitle - if let timerString = liveLocationParameters.timer { - liveLocationTimerLabel.isHidden = false - liveLocationTimerLabel.text = timerString - } else { - liveLocationTimerLabel.isHidden = true - } - stopSharingButton.isHidden = !liveLocationParameters.shouldShowStopButton + + liveLocationImageView.image = Asset.Images.locationLiveCellIcon.image + liveLocationImageView.tintColor = bannerViewData.iconTint + + liveLocationStatusLabel.text = bannerViewData.title + liveLocationStatusLabel.textColor = bannerViewData.titleColor + + liveLocationTimerLabel.text = bannerViewData.timeLeftString + liveLocationTimerLabel.isHidden = !bannerViewData.showTimer + + rightButton.setTitle(bannerViewData.rightButtonTitle, for: .normal) + rightButton.isHidden = !bannerViewData.showRightButton + rightButton.tag = bannerViewData.rightButtonTag.rawValue + + placeholderImage.image = bannerViewData.placeholderImage + placeholderImage.isHidden = !bannerViewData.showPlaceholderImage + mapView.isHidden = bannerViewData.showPlaceholderImage } + private func liveLocationBannerViewData(from viewState: TimelineLiveLocationViewState) -> LiveLocationBannerViewData { + let theme = ThemeService.shared().theme + + let iconTint: UIColor + let title: String + var titleColor: UIColor = theme.roomCellLocalisationTextColor + var placeholderImage: UIImage? + var timeLeftString: String? + var rightButtonTitle: String? + var rightButtonTag: RightButtonTag = .stopSharing + + switch viewState { + case .incoming(let liveLocationSharingStatus): + switch liveLocationSharingStatus { + case .started(let timeLeft): + iconTint = theme.roomCellLocalisationStartedColor + title = VectorL10n.liveLocationSharingBannerTitle + timeLeftString = generateTimerString(for: timeLeft, isIncomingLocation: true) + case .stopped: + iconTint = theme.roomCellLocalisationEndedColor + title = VectorL10n.liveLocationSharingEnded + titleColor = theme.roomCellLocalisationEndedColor + placeholderImage = Asset.Images.locationLiveCellEndedImage.image + } + case .outgoing(let liveLocationSharingStatus): + switch liveLocationSharingStatus { + case .starting: + iconTint = theme.roomCellLocalisationEndedColor + title = VectorL10n.locationSharingLiveLoading + titleColor = theme.roomCellLocalisationEndedColor + placeholderImage = Asset.Images.locationLiveCellLoadingImage.image + case .started(let timeLeft): + iconTint = theme.roomCellLocalisationStartedColor + title = VectorL10n.liveLocationSharingBannerTitle + timeLeftString = generateTimerString(for: timeLeft, isIncomingLocation: false) + rightButtonTitle = VectorL10n.stop + case .failure: + iconTint = theme.roomCellLocalisationErrorColor + title = VectorL10n.locationSharingLiveError + titleColor = theme.roomCellLocalisationEndedColor + rightButtonTitle = VectorL10n.retry + rightButtonTag = .retrySharing + case .stopped: + iconTint = theme.roomCellLocalisationEndedColor + title = VectorL10n.liveLocationSharingEnded + titleColor = theme.roomCellLocalisationEndedColor + placeholderImage = Asset.Images.locationLiveCellEndedImage.image + } + } + + return LiveLocationBannerViewData(placeholderImage: placeholderImage, iconTint: iconTint, title: title, titleColor: titleColor, timeLeftString: timeLeftString, rightButtonTitle: rightButtonTitle, rightButtonTag: rightButtonTag) + } + + private func generateTimerString(for timestamp: Double, + isIncomingLocation: Bool) -> String? { + let timerString: String? + if isIncomingLocation { + timerString = VectorL10n.locationSharingLiveTimerIncoming(incomingTimerFormatter.string(from: Date(timeIntervalSince1970: timestamp))) + } else if let outgoingTimer = outgoingTimerFormatter.string(from: Date(timeIntervalSince1970: timestamp).timeIntervalSinceNow) { + timerString = VectorL10n.locationSharingLiveTimerOutgoing(outgoingTimer) + } else { + timerString = nil + } + return timerString + } + + // MARK: - Public + + public func displayStaticLocation(with viewData: RoomTimelineLocationViewData) { + displayLocation(viewData.location, + userAvatarData: viewData.userAvatarData, + mapStyleURL: viewData.mapStyleURL, + bannerViewData: nil) + } + + public func displayLiveLocation(with viewData: RoomTimelineLocationViewData, liveLocationViewState: TimelineLiveLocationViewState) { + let bannerViewData = liveLocationBannerViewData(from: liveLocationViewState) + displayLocation(viewData.location, + userAvatarData: viewData.userAvatarData, + mapStyleURL: viewData.mapStyleURL, + bannerViewData: bannerViewData) + + } + + // MARK: - Themable func update(theme: Theme) { @@ -167,7 +311,11 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat // MARK: - Action - @IBAction private func stopSharingAction(_ sender: Any) { - // TODO: - Stop sharing action + @IBAction private func didTapTightButton(_ sender: Any) { + if rightButton.tag == RightButtonTag.stopSharing.rawValue { + delegate?.didTapStopButton() + } else if rightButton.tag == RightButtonTag.retrySharing.rawValue { + delegate?.didTapRetryButton() + } } } diff --git a/Riot/Modules/Room/Location/RoomTimelineLocationView.xib b/Riot/Modules/Room/Location/RoomTimelineLocationView.xib index c6cdf2ed3..a149fcead 100644 --- a/Riot/Modules/Room/Location/RoomTimelineLocationView.xib +++ b/Riot/Modules/Room/Location/RoomTimelineLocationView.xib @@ -4,7 +4,6 @@ - @@ -15,6 +14,9 @@ + @@ -41,7 +43,7 @@ - + @@ -77,7 +79,7 @@ - + @@ -129,11 +131,14 @@ - + + + + @@ -148,13 +153,14 @@ - + + - + diff --git a/Riot/Modules/Room/MXKRoomViewController.m b/Riot/Modules/Room/MXKRoomViewController.m index a8d79f084..ba1dd3e21 100644 --- a/Riot/Modules/Room/MXKRoomViewController.m +++ b/Riot/Modules/Room/MXKRoomViewController.m @@ -3045,6 +3045,22 @@ [self promptUserToResendEvent:selectedEvent.eventId]; } } + else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellStopShareButtonPressed]) + { + MXEvent *selectedEvent = userInfo[kMXKRoomBubbleCellEventKey]; + if (selectedEvent) + { + // TODO: - Implement stop live location action + } + } + else if ([actionIdentifier isEqualToString:kMXKRoomBubbleCellRetryShareButtonPressed]) + { + MXEvent *selectedEvent = userInfo[kMXKRoomBubbleCellEventKey]; + if (selectedEvent) + { + // TODO: - Implement retry live location action + } + } } #pragma mark - Clipboard diff --git a/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.h b/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.h index 8e247835f..15e18f89b 100644 --- a/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.h +++ b/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.h @@ -83,6 +83,20 @@ extern NSString *const kMXKRoomBubbleCellTapOnContentView; */ extern NSString *const kMXKRoomBubbleCellUnsentButtonPressed; +/** + Action identifier used when the user pressed stop share button displayed in live location cell. + + The `userInfo` dictionary contains an `MXEvent` object under the `kMXKRoomBubbleCellEventKey` key, representing the live location event to stop. + */ +extern NSString *const kMXKRoomBubbleCellStopShareButtonPressed; + +/** + Action identifier used when the user pressed retry share button displayed in live location cell. + + The `userInfo` dictionary contains an `MXEvent` object under the `kMXKRoomBubbleCellEventKey` key, representing the live location event to retry. + */ +extern NSString *const kMXKRoomBubbleCellRetryShareButtonPressed; + /** Action identifier used when the user long pressed on a displayed event. diff --git a/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.m b/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.m index c4367d598..f5fcdc4ff 100644 --- a/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.m +++ b/Riot/Modules/Room/TimelineCells/Common/MXKRoomBubbleTableViewCell.m @@ -41,7 +41,10 @@ NSString *const kMXKRoomBubbleCellTapOnAttachmentView = @"kMXKRoomBubbleCellTapO NSString *const kMXKRoomBubbleCellTapOnOverlayContainer = @"kMXKRoomBubbleCellTapOnOverlayContainer"; NSString *const kMXKRoomBubbleCellTapOnContentView = @"kMXKRoomBubbleCellTapOnContentView"; + NSString *const kMXKRoomBubbleCellUnsentButtonPressed = @"kMXKRoomBubbleCellUnsentButtonPressed"; +NSString *const kMXKRoomBubbleCellStopShareButtonPressed = @"kMXKRoomBubbleCellStopShareButtonPressed"; +NSString *const kMXKRoomBubbleCellRetryShareButtonPressed = @"kMXKRoomBubbleCellRetryShareButtonPressed"; NSString *const kMXKRoomBubbleCellLongPressOnEvent = @"kMXKRoomBubbleCellLongPressOnEvent"; NSString *const kMXKRoomBubbleCellLongPressOnProgressView = @"kMXKRoomBubbleCellLongPressOnProgressView"; diff --git a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Location/LocationPlainCell.swift b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Location/LocationPlainCell.swift index 4d47dd775..a8f752f59 100644 --- a/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Location/LocationPlainCell.swift +++ b/Riot/Modules/Room/TimelineCells/Styles/Plain/Cells/Location/LocationPlainCell.swift @@ -20,6 +20,7 @@ import MatrixSDK class LocationPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable, RoomCellReadMarkerDisplayable { private var locationView: RoomTimelineLocationView! + private var event: MXEvent? override func render(_ cellData: MXKCellData!) { super.render(cellData) @@ -31,6 +32,7 @@ class LocationPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable, Room return } + self.event = event locationView.update(theme: ThemeService.shared().theme) // Comment this line and uncomment next one to test UI of live location tile @@ -49,17 +51,19 @@ class LocationPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable, Room let mapStyleURL = bubbleData.mxSession.vc_homeserverConfiguration().tileServer.mapStyleURL + let avatarViewData: AvatarViewData? + if locationContent.assetType == .user { - let avatarViewData = AvatarViewData(matrixItemId: bubbleData.senderId, + avatarViewData = AvatarViewData(matrixItemId: bubbleData.senderId, displayName: bubbleData.senderDisplayName, avatarUrl: bubbleData.senderAvatarUrl, mediaManager: bubbleData.mxSession.mediaManager, fallbackImage: .matrixItem(bubbleData.senderId, bubbleData.senderDisplayName)) - - locationView.displayLocation(location, userAvatarData: avatarViewData, mapStyleURL: mapStyleURL) } else { - locationView.displayLocation(location, mapStyleURL: mapStyleURL) + avatarViewData = nil } + + locationView.displayStaticLocation(with: RoomTimelineLocationViewData(location: location, userAvatarData: avatarViewData, mapStyleURL: mapStyleURL)) } private func renderLiveLocation(_ event: MXEvent) { @@ -84,36 +88,11 @@ class LocationPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable, Room mediaManager: bubbleData.mxSession.mediaManager, fallbackImage: .matrixItem(bubbleData.senderId, bubbleData.senderDisplayName)) let futurDateTimeInterval = Date(timeIntervalSinceNow: 3734).timeIntervalSince1970 - locationView.displayLocation(location, userAvatarData: avatarViewData, mapStyleURL: mapStyleURL, liveLocationState: .outgoingLive(generateTimerString(for: futurDateTimeInterval, isIncomingLocation: false))) + + locationView.displayLiveLocation(with: RoomTimelineLocationViewData(location: location, userAvatarData: avatarViewData, mapStyleURL: mapStyleURL), + liveLocationViewState: .outgoing(.failure)) } - private func generateTimerString(for timestamp: Double, - isIncomingLocation: Bool) -> String? { - let timerString: String? - if isIncomingLocation { - timerString = VectorL10n.locationSharingLiveTimerIncoming(incomingTimerFormatter.string(from: Date(timeIntervalSince1970: timestamp))) - } else if let outgoingTimer = outgoingTimerFormatter.string(from: Date(timeIntervalSince1970: timestamp).timeIntervalSinceNow) { - timerString = VectorL10n.locationSharingLiveTimerOutgoing(outgoingTimer) - } else { - timerString = nil - } - return timerString - } - - private lazy var incomingTimerFormatter: DateFormatter = { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "HH:mm" - return dateFormatter - }() - - private lazy var outgoingTimerFormatter: DateComponentsFormatter = { - let formatter = DateComponentsFormatter() - formatter.zeroFormattingBehavior = .dropAll - formatter.allowedUnits = [.hour, .minute, .second] - formatter.unitsStyle = .brief - return formatter - }() - override func setupViews() { super.setupViews() @@ -131,3 +110,21 @@ class LocationPlainCell: SizableBaseRoomCell, RoomCellReactionsDisplayable, Room contentView.vc_addSubViewMatchingParent(locationView) } } + +extension LocationPlainCell: RoomTimelineLocationViewDelegate { + func didTapStopButton() { + guard let event = self.event else { + return + } + + delegate.cell(self, didRecognizeAction: kMXKRoomBubbleCellStopShareButtonPressed, userInfo: [kMXKRoomBubbleCellEventKey: event]) + } + + func didTapRetryButton() { + guard let event = self.event else { + return + } + + delegate.cell(self, didRecognizeAction: kMXKRoomBubbleCellRetryShareButtonPressed, userInfo: [kMXKRoomBubbleCellEventKey: event]) + } +} From 399c12652a795350df55a6c78b0c51fc10c8b086 Mon Sep 17 00:00:00 2001 From: Christian Woltering Date: Sun, 24 Apr 2022 10:47:06 +0000 Subject: [PATCH 075/158] Translated using Weblate (German) Currently translated at 92.2% (1857 of 2014 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/de/ --- Riot/Assets/de.lproj/Vector.strings | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Riot/Assets/de.lproj/Vector.strings b/Riot/Assets/de.lproj/Vector.strings index 4fedaa943..bc5476a65 100644 --- a/Riot/Assets/de.lproj/Vector.strings +++ b/Riot/Assets/de.lproj/Vector.strings @@ -224,7 +224,7 @@ "settings_add_email_address" = "E-Mail-Adresse hinzufügen"; "settings_remove_phone_prompt_msg" = "Bist du sicher, dass du die Telefon-Nummer %@ entfernen möchtest?"; "settings_night_mode" = "Nachtmodus"; -"settings_enable_push_notif" = "Benachrichtungen auf diesem Gerät"; +"settings_enable_push_notif" = "Benachrichtigungen auf diesem Gerät"; "settings_unignore_user" = "Alle Nachrichten von %@ anzeigen?"; "settings_contacts_phonebook_country" = "Land des Telefonbuches"; "settings_labs_e2e_encryption" = "Ende-zu-Ende-Verschlüsselung"; @@ -380,7 +380,7 @@ "call_incoming_voice_prompt" = "Eingehender Sprachanruf von %@"; "call_incoming_video_prompt" = "Eingehender Videoanruf von %@"; // No VoIP support -"no_voip_title" = "Eingehener Anruf"; +"no_voip_title" = "Eingehender Anruf"; "no_voip" = "%@ ruft dich an, aber %@ unterstützt derzeit keine Anrufe.\nDu kannst diese Benachrichtung ignorieren und den Anruf von einem anderen Gerät annehmen oder ihn abweisen."; // Crash report "google_analytics_use_prompt" = "Möchtest du helfen %@ zu verbessern, indem du anonyme Absturzberichte und Daten über die Verwendung sendest?"; @@ -531,7 +531,7 @@ "event_formatter_rerequest_keys_part2" = " von deinen anderen Sitzungen anfragen."; // Re-request confirmation dialog "rerequest_keys_alert_title" = "Anfrage gesendet"; -"rerequest_keys_alert_message" = "Bitte %@ auf einem anderen Gerät öffenen, das die Nachricht entschlüsseln kann, damit es die Schlüssel an diese Sitzung senden kann."; +"rerequest_keys_alert_message" = "Bitte %@ auf einem anderen Gerät öffnen, das die Nachricht entschlüsseln kann, damit es die Schlüssel an diese Sitzung senden kann."; "room_message_reply_to_placeholder" = "Antwort senden (unverschlüsselt)…"; "encrypted_room_message_reply_to_placeholder" = "Sende eine verschlüsselte Antwort…"; "room_message_reply_to_short_placeholder" = "Sende eine Antwort…"; @@ -738,7 +738,7 @@ "auth_softlogout_recover_encryption_keys" = "Melde dich an, um ausschließlich auf diesem Gerät gespeicherte Verschlüsselungsschlüssel wiederherzustellen. Du benötigst sie, um deine verschlüsselten Nachrichten auf jedem Gerät zu lesen."; "auth_softlogout_clear_data" = "Persönliche Daten löschen"; "auth_softlogout_clear_data_message_1" = "Warnung: Deine persönlichen Daten (einschließlich Verschlüsselungsschlüssel) sind noch auf diesem Gerät gespeichert."; -"auth_softlogout_clear_data_message_2" = "Deaktiviere es, wenn du dieses Gerät nicht mehr verwendest oder du dich mit einem anderen Konto anmelden möchtst."; +"auth_softlogout_clear_data_message_2" = "Deaktiviere es, wenn du dieses Gerät nicht mehr verwendest oder du dich mit einem anderen Konto anmelden möchtest."; "auth_softlogout_clear_data_button" = "Lösche alle Daten"; "auth_softlogout_clear_data_sign_out_title" = "Bist du sicher?"; "auth_softlogout_clear_data_sign_out_msg" = "Möchtest du wirklich alle derzeit auf diesem Gerät gespeicherten Daten löschen? Melde dich erneut an, um auf deine Kontodaten und Nachrichten zuzugreifen."; @@ -1371,7 +1371,7 @@ "user_avatar_view_accessibility_label" = "Avatar"; "secrets_recovery_with_key_information_unlock_secure_backup_with_key" = "Zum Fortfahren gib deinen Sicherheitsschlüssel ein."; -"secrets_recovery_with_key_information_unlock_secure_backup_with_phrase" = "Zum Forfahren gib deine Passphrase ein."; +"secrets_recovery_with_key_information_unlock_secure_backup_with_phrase" = "Zum Fortfahren gib deine Passphrase ein."; // Success from secure backup "key_backup_setup_success_from_secure_backup_info" = "Deine Schlüssel werden gesichert."; @@ -1644,8 +1644,8 @@ /* The placeholder string contains onboarding_use_case_skip_button as a tappable action */ "onboarding_use_case_not_sure_yet" = "Noch nicht sicher? Du kannst"; "onboarding_use_case_skip_button" = "diese Frage überspringen"; -"onboarding_use_case_existing_server_message" = "Willst du einen existierenden Server beitreten?"; -"onboarding_use_case_existing_server_button" = "MIt Server verbinden"; +"onboarding_use_case_existing_server_message" = "Willst du einem existierenden Server beitreten?"; +"onboarding_use_case_existing_server_button" = "Mit Server verbinden"; "search_filter_placeholder" = "Filtern"; @@ -1855,8 +1855,8 @@ "room_please_select" = "Bitte wähle einen Raum"; "room_error_join_failed_title" = "Konnte Raum nicht betreten"; "room_error_join_failed_empty_room" = "Es ist aktuell nicht möglich einen leeren Raum zu betreten."; -"room_error_name_edition_not_authorized" = "Du bist nicht authorisiert den Raumnamen zu ändern"; -"room_error_topic_edition_not_authorized" = "Du bist nicht authorisiert das Raumthema zu ändern"; +"room_error_name_edition_not_authorized" = "Du bist nicht autorisiert den Raumnamen zu ändern"; +"room_error_topic_edition_not_authorized" = "Du bist nicht autorisiert das Raumthema zu ändern"; "room_error_cannot_load_timeline" = "Konnte Verlauf nicht laden"; "room_error_timeline_event_not_found_title" = "Konnte Position im Verlauf nicht laden"; "room_error_timeline_event_not_found" = "Konnte spezifischen Punkt im Verlauf dieses Raumes nicht finden"; @@ -1872,7 +1872,7 @@ "message_reply_to_message_to_reply_to_prefix" = "Als Antwort auf"; // Room members "room_member_ignore_prompt" = "Sicher, dass alle Nachrichten von diesem Benutzer versteckt werden sollen?"; -"room_member_power_level_prompt" = "Du kannst diese Änderung nicht rückgangig machen, weil du dem Benutzer die gleiche Berechtigungsstufe gibst, die du selbst hast.\nBist du sicher?"; +"room_member_power_level_prompt" = "Du kannst diese Änderung nicht rückgängig machen, weil du dem Benutzer die gleiche Berechtigungsstufe gibst, die du selbst hast.\nBist du sicher?"; // Attachment "attachment_size_prompt" = "Möchtest du senden als:"; "attachment_original" = "Originalgröße (%@)"; @@ -1904,7 +1904,7 @@ "e2e_passphrase_enter" = "Passphrase eingeben"; // E2E export "e2e_export_room_keys" = "Exportiere Raumschlüssel"; -"e2e_export_prompt" = "Dieser Prozeß erlaubt den Export von Schlüsseln, die du in verschlüsselten Räumen empfangen hast, in eine lokale Datei. Du kannst dann die Datei in einem anderen Matrixclient in Zukunft importieren, so dass dieser Client die Nachrichten auch entschlüsseln kann.\nDie exportierte Datei wird jedem der sie lesen kann erlauben, alle verschlüsselten Nachrichten sehen können, also verwahre die Datei sicher."; +"e2e_export_prompt" = "Dieser Prozess erlaubt den Export von Schlüsseln, die du in verschlüsselten Räumen empfangen hast, in eine lokale Datei. Du kannst dann die Datei in einem anderen Matrixclient in Zukunft importieren, so dass dieser Client die Nachrichten auch entschlüsseln kann.\nDie exportierte Datei wird jedem der sie lesen kann erlauben, alle verschlüsselten Nachrichten sehen können, also verwahre die Datei sicher."; "e2e_export" = "Exportiere"; "e2e_passphrase_confirm" = "Passphrase bestätigen"; "e2e_passphrase_empty" = "Die Passphrase darf nicht leer sein"; @@ -2086,7 +2086,7 @@ "notification_settings_receive_a_call" = "Benachrichtige, wenn ich einen Anruf erhalte"; "notification_settings_suppress_from_bots" = "Unterdrücke Benachrichtigungen von Bots"; "notification_settings_by_default" = "Als Standard..."; -"notification_settings_notify_all_other" = "Benachrichtige für alle andereren Nachrichten/Räume"; +"notification_settings_notify_all_other" = "Benachrichtige für alle anderen Nachrichten/Räume"; // gcm section // Settings keys @@ -2114,7 +2114,7 @@ "call_more_actions_change_audio_device" = "Audiogerät ändern"; "call_more_actions_unhold" = "Fortsetzen"; "call_more_actions_hold" = "Halten"; -"call_holded" = "Du hast den Anurf pausiert"; +"call_holded" = "Du hast den Anruf pausiert"; "call_remote_holded" = "%@ hat den Anruf pausiert"; "notice_declined_video_call_by_you" = "Du hast den Anruf abgelehnt"; "notice_declined_video_call" = "%@ hat den Anruf abgelehnt"; From ec51eb89b05b7ecc3433eefd65d594ea9d7e5a0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sveinn=20=C3=AD=20Felli?= Date: Fri, 22 Apr 2022 11:59:46 +0000 Subject: [PATCH 076/158] Translated using Weblate (Icelandic) Currently translated at 78.4% (1580 of 2014 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/is/ --- Riot/Assets/is.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/is.lproj/Vector.strings b/Riot/Assets/is.lproj/Vector.strings index 9ad78706f..009f73c70 100644 --- a/Riot/Assets/is.lproj/Vector.strings +++ b/Riot/Assets/is.lproj/Vector.strings @@ -1293,7 +1293,7 @@ "settings_call_invitations" = "Boð um símtöl"; "settings_room_invitations" = "Boð á spjallrás"; "settings_messages_containing_keywords" = "Stikkorð"; -"settings_messages_containing_at_room" = "@spjallrás"; +"settings_messages_containing_at_room" = "@room"; "settings_messages_containing_user_name" = "Notandanafnið mitt"; "settings_messages_containing_display_name" = "Birtingarnafn mitt"; "settings_encrypted_group_messages" = "Dulrituð hópskilaboð"; From 789e41f31fba8bb90b3e5d670c4959804554ed5f Mon Sep 17 00:00:00 2001 From: lvre <7uu3qrbvm@relay.firefox.com> Date: Sat, 23 Apr 2022 00:32:50 +0000 Subject: [PATCH 077/158] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (2014 of 2014 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/pt_BR/ --- Riot/Assets/pt_BR.lproj/Vector.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Assets/pt_BR.lproj/Vector.strings b/Riot/Assets/pt_BR.lproj/Vector.strings index e74a50484..4beaf6b90 100644 --- a/Riot/Assets/pt_BR.lproj/Vector.strings +++ b/Riot/Assets/pt_BR.lproj/Vector.strings @@ -1576,7 +1576,7 @@ "ok" = "OK"; "onboarding_splash_page_1_message" = "Comunicação segura e independente que lhe dá o mesmo nível de privacidade que uma conversa face-a-face em sua própria casa."; "settings_enable_room_message_bubbles" = "Bolhas de mensagem"; -"onboarding_splash_page_4_message" = "Element também é ótimo para o lugar de trabalho. É confiado pelas organizações mais seguras do mundo."; +"onboarding_splash_page_4_message" = "Element também é ótimo para o lugar de trabalho. Ele é confiado pelas organizações mais seguras do mundo."; "onboarding_splash_page_4_title_no_pun" = "Mensageria para seu time."; "onboarding_splash_page_3_message" = "Encriptado ponta-a-ponta e nenhum número de telefone requerido. Sem publicidade ou datamining."; "onboarding_splash_page_3_title" = "Mensageria segura."; From a0381e55d2a3558eb8f45258b9eae712d348f027 Mon Sep 17 00:00:00 2001 From: LinAGKar Date: Sat, 23 Apr 2022 12:05:21 +0000 Subject: [PATCH 078/158] Translated using Weblate (Swedish) Currently translated at 100.0% (2014 of 2014 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/sv/ --- Riot/Assets/sv.lproj/Vector.strings | 52 ++++++++++++++--------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/Riot/Assets/sv.lproj/Vector.strings b/Riot/Assets/sv.lproj/Vector.strings index 04a4e5f0f..7e4dad115 100644 --- a/Riot/Assets/sv.lproj/Vector.strings +++ b/Riot/Assets/sv.lproj/Vector.strings @@ -1165,8 +1165,8 @@ "create_room_title" = "Nytt rum"; "create_room_section_header_name" = "NAMN"; "create_room_placeholder_name" = "Namn"; -"create_room_section_header_topic" = "Samtalsämne (valfritt)"; -"create_room_placeholder_topic" = "Vad är rummets samtalsämne?"; +"create_room_section_header_topic" = "ÄMNE (VALFRITT)"; +"create_room_placeholder_topic" = "Vad handlar rummet om?"; "create_room_section_header_encryption" = "KRYPTERING"; "create_room_enable_encryption" = "Aktivera kryptering"; "create_room_section_footer_encryption" = "Kryptering kan inte inaktiveras igen."; @@ -2075,7 +2075,7 @@ "room_participants_leave_processing" = "Lämnar"; "spaces_creation_new_rooms_support" = "Support"; "create_room_promotion_header" = "BEFORDRAN"; -"room_access_settings_screen_upgrade_alert_message" = "Vem som helst i utrymmet kommer kunna hitta och gå med i det här rummet – man behöver inte bjuda in alla manuellt. Du kommer kunna ändra dessa inställningar för rummet när som helst.\n\nNotera att en ny version av rummet kommer skapas vid uppgradering. Alla existerande meddelanden kommer behållas arkiverade i detta rummet."; +"room_access_settings_screen_upgrade_alert_message" = "Vem som helst i utrymmet kommer kunna hitta och gå med i det här rummet – man behöver inte bjuda in alla manuellt. Du kommer kunna ändra detta i rumsinställningarna när som helst.\n\nObservera att en ny version av rummet kommer skapas vid uppgradering. Alla existerande meddelanden kommer behållas arkiverade i detta rum."; "onboarding_celebration_button" = "Nu kör vi"; "ignore_user" = "Ignorera användare"; "location_sharing_pin_drop_share_title" = "Skicka denna position"; @@ -2087,7 +2087,7 @@ "location_sharing_live_share_title" = "Dela position i realtid"; "side_menu_coach_message" = "Svep åt höger eller tryck för att se alla rum"; -"spaces_add_room_missing_permission_message" = "Du har inte behörighet att lägga till rum i detta utrymmet."; +"spaces_add_room_missing_permission_message" = "Du är inte behörig att lägga till rum i detta utrymme."; "spaces_creation_in_one_space" = "i 1 utrymme"; "spaces_creation_in_many_spaces" = "i %@ utrymmen"; "spaces_creation_in_spacename_plus_many" = "i %@ + %@ utrymmen"; @@ -2102,9 +2102,9 @@ "spaces_creation_invite_by_username_message" = "Du kan bjuda in dem senare också."; "spaces_creation_invite_by_username_title" = "Bjud in ditt team"; "spaces_creation_invite_by_username" = "Bjud in genom användarnamn"; -"spaces_creation_add_rooms_message" = "Eftersom detta utrymmet endast är för dig så kommer ingen annan informeras. Du kan lägga till fler senare."; +"spaces_creation_add_rooms_message" = "Eftersom det här utrymmet endast är för dig så kommer ingen annan informeras. Du kan lägga till fler senare."; "spaces_creation_add_rooms_title" = "Vad skulle du vilja lägga till?"; -"spaces_creation_sharing_type_me_and_teammates_detail" = "Ett privat utrymme för dig & dina teamkompisar"; +"spaces_creation_sharing_type_me_and_teammates_detail" = "Ett privat utrymme för dig & dina teamkamrater"; "spaces_creation_sharing_type_me_and_teammates_title" = "Jag och andra i mitt team"; "spaces_creation_sharing_type_just_me_detail" = "Ett privat utrymme för att organisera dina rum"; "spaces_creation_sharing_type_just_me_title" = "Endast jag"; @@ -2125,18 +2125,18 @@ "spaces_creation_address_already_exists" = "%@\nfinns redan"; "spaces_creation_address_invalid_characters" = "%@\nhar ogiltiga tecken"; "spaces_creation_address_default_message" = "Ditt utrymme kommer kunna ses på\n%@"; -"spaces_creation_empty_room_name_error" = "Namn är obligatoriskt"; +"spaces_creation_empty_room_name_error" = "Namn krävs"; "spaces_creation_address" = "Adress"; "spaces_creation_settings_message" = "Lägg till lite mer info för att synas. Du kan ändra detta när som helst."; "spaces_creation_footer" = "Du kan ändra detta senare"; "spaces_creation_visibility_message" = "För att gå med i ett existerande utrymme så behöver du en inbjudan."; -"spaces_creation_visibility_title" = "Vilket typ av utrymme vill du skapa?"; +"spaces_creation_visibility_title" = "Vilken typ av utrymme vill du skapa?"; // Mark: - Space Creation "spaces_creation_hint" = "Utrymmen är ett nytt sätt att gruppera rum och personer."; "space_settings_current_address_message" = "Ditt utrymme kan ses på\n%@"; -"space_settings_update_failed_message" = "Kunde ej uppdatera inställningar för utrymmet. Vill du försöka igen?"; +"space_settings_update_failed_message" = "Misslyckades att uppdatera inställningar för utrymmet. Vill du försöka igen?"; "space_settings_access_section" = "Vem har åtkomst till detta utrymmet?"; "space_topic" = "Beskrivning"; "space_public_join_rule_detail" = "Öppet för alla, passar bäst för öppna grupper"; @@ -2148,10 +2148,10 @@ "spaces_explore_rooms_room_number" = "%@ rum"; "spaces_create_space_title" = "Skapa ett utrymme"; "spaces_add_space_title" = "Skapa utrymme"; -"space_invite_not_enough_permission" = "Du har inte behörighet att bjuda in andra till detta utrymmet"; -"room_invite_not_enough_permission" = "Du har inte behörighet att bjuda in andra till detta rummet"; +"space_invite_not_enough_permission" = "Du är inte behörig att bjuda in andra till detta utrymme"; +"room_invite_not_enough_permission" = "Du är inte behörig att bjuda in andra till detta rum"; "room_invite_to_room_option_detail" = "De kommer inte bli del av %@."; -"room_invite_to_room_option_title" = "Endast till detta rummet"; +"room_invite_to_room_option_title" = "Endast till detta rum"; "room_invite_to_space_option_detail" = "De kommer kunna utforska %@, men kommer inte bli medlemmar i %@."; // Mark: - Room invite @@ -2165,13 +2165,13 @@ "share_invite_link_action" = "Dela inbjudningslänk"; "create_room_processing" = "Skapar rum"; "create_room_suggest_room_footer" = "Rekommenderade rum visas för medlemmar i utrymmet som förslag på bra ställen att gå med."; -"create_room_suggest_room" = "Föreslå medlemmar till utrymmet"; +"create_room_suggest_room" = "Föreslå för utrymmesmedlemmar"; "create_room_show_in_directory_footer" = "Detta kommer underlätta för andra att hitta och gå med."; "create_room_section_footer_type_public" = "Endast inbjudna kan hitta och gå med, inte bara personer i utrymmet."; "create_room_section_footer_type_restricted" = "Vem som helst i utrymmet kan hitta och gå med."; "create_room_section_footer_type_private" = "Endast inbjudna personer kan hitta och gå med."; "create_room_type_restricted" = "Utrymmesmedlemmar"; -"call_jitsi_unable_to_start" = "Kunde ej starta flerpartssamtal"; +"call_jitsi_unable_to_start" = "Kunde inte starta gruppsamtal"; "room_suggestion_settings_screen_message" = "Rekommenderade rum visas för medlemmar i utrymmet, som bra förslag på bra ställen att gå med i."; "room_suggestion_settings_screen_title" = "Gör så att ett rum blir rekommenderat i ett utrymme"; @@ -2179,33 +2179,33 @@ "room_suggestion_settings_screen_nav_title" = "Föreslå rum"; "room_access_space_chooser_other_spaces_section_info" = "Administratörer i %@ är troligen även med i dessa."; "room_access_space_chooser_other_spaces_section" = "Andra utrymmen eller rum"; -"room_access_space_chooser_known_spaces_section" = "Utrymmen som du vet innehåller %@"; -"room_access_settings_screen_setting_room_access" = "Ställ in rumsåtkomst"; -"room_access_settings_screen_upgrade_alert_upgrading" = "Uppdatera rum"; -"room_access_settings_screen_upgrade_alert_upgrade_button" = "Uppdatera"; +"room_access_space_chooser_known_spaces_section" = "Utrymmen du känner till som innehåller %@"; +"room_access_settings_screen_setting_room_access" = "Ställer in rumsåtkomst"; +"room_access_settings_screen_upgrade_alert_upgrading" = "Uppgraderar rum"; +"room_access_settings_screen_upgrade_alert_upgrade_button" = "Uppgradera"; "room_access_settings_screen_upgrade_alert_auto_invite_switch" = "Bjud automatiskt in medlemmar till nya rum"; "room_access_settings_screen_upgrade_alert_title" = "Uppgradera rum"; "room_access_settings_screen_public_message" = "Vem som helst kan hitta och gå med."; "room_access_settings_screen_edit_spaces" = "Redigera utrymmen"; -"room_access_settings_screen_upgrade_required" = "Uppdatering krävs"; +"room_access_settings_screen_upgrade_required" = "Uppgradering krävs"; "room_access_settings_screen_restricted_message" = "Tillåt vem som helst i ett utrymme att hitta och gå med.\nDu kommer behöva bekräfta vilka utrymmen det gäller."; "room_access_settings_screen_private_message" = "Endast inbjudna kan hitta och gå med."; -"room_access_settings_screen_message" = "Välj vem som kan hitta och gå med i %@."; -"room_access_settings_screen_title" = "Vem kan komma åt detta rummet?"; +"room_access_settings_screen_message" = "Välj vilka som kan hitta och gå med i %@."; +"room_access_settings_screen_title" = "Vika kan komma åt detta rum?"; // Room Access Settings "room_access_settings_screen_nav_title" = "Rumsåtkomst"; "room_details_promote_room_suggest_title" = "Föreslå utrymme för medlemmar"; "room_details_promote_room_title" = "Befordra rum"; "room_details_access_row_title" = "Åtkomst"; -"settings_labs_enable_auto_report_decryption_errors" = "Rapportera krypteringsfel automatiskt"; -"room_preview_decline_invitation_options" = "Vill du avböja inbjudan eller ignorera denna användaren?"; +"settings_labs_enable_auto_report_decryption_errors" = "Rapportera avkrypteringsfel automatiskt"; +"room_preview_decline_invitation_options" = "Vill du avböja inbjudan eller ignorera denna användare?"; "threads_beta_cancel" = "Inte nu"; "threads_beta_enable" = "Prova"; "threads_beta_information_link" = "Läs mer"; "threads_beta_information" = "Håll diskussioner organiserade med trådar.\n\nTrådar hjälper dig hålla konversationer till ämnet och gör dem lättare att följa. "; "threads_beta_title" = "Trådar"; -"threads_notice_done" = "Jag fattar"; +"threads_notice_done" = "Jag förstår"; "threads_notice_information" = "Alla trådar som skapades under experimentperioden kommer nu att skrivas ut som vanliga svar.

    Detta kommer vara en engångshändelse under övergången, eftersom trådar nu är en del av Matrix-specen."; "threads_notice_title" = "Trådar är inte längre experimentella 🎉"; "room_participants_invite_prompt_to_msg" = "Är du säker på att du vill bjuda in %@ till %@?"; @@ -2214,12 +2214,12 @@ "onboarding_avatar_accessibility_label" = "Profilbild"; "onboarding_avatar_message" = "Du kan ändra detta när som helst."; "onboarding_avatar_title" = "Lägg till en profilbild"; -"onboarding_display_name_max_length" = "Ditt visningsnamn måste vara kortare än 256 karaktärer"; +"onboarding_display_name_max_length" = "Ditt visningsnamn måste vara kortare än 256 tecken"; "onboarding_display_name_hint" = "Du kan ändra detta senare"; "onboarding_display_name_placeholder" = "Visningsnamn"; "onboarding_display_name_message" = "Detta kommer visas när du skickar meddelanden."; "onboarding_display_name_title" = "Välj visningsnamn"; -"onboarding_personalization_skip" = "Hoppa över steg"; +"onboarding_personalization_skip" = "Hoppa över steget"; "onboarding_personalization_save" = "Spara och fortsätt"; "onboarding_congratulations_home_button" = "Ta mig hem"; "onboarding_congratulations_personalize_button" = "Personifiera profil"; From 1734b28d35bde84cb87b2b61ab4e8a12b871b367 Mon Sep 17 00:00:00 2001 From: MaximeE Date: Mon, 25 Apr 2022 17:21:27 +0200 Subject: [PATCH 079/158] 6029: Modify color for dark mode --- .../Location/RoomTimelineLocationView.swift | 30 ++++++++++--------- .../Location/RoomTimelineLocationView.xib | 12 +++++++- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift index 4665fbc18..0c7fcf3cf 100644 --- a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift +++ b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift @@ -108,6 +108,7 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat private var mapView: MGLMapView! private var annotationView: LocationMarkerView? private static var usernameColorGenerator = UserNameColorGenerator() + private static var theme = ThemeService.shared().theme private lazy var incomingTimerFormatter: DateFormatter = { let dateFormatter = DateFormatter() @@ -189,6 +190,7 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat } liveLocationContainerView.isHidden = false + liveLocationContainerView.backgroundColor = Self.theme.colors.background.withAlphaComponent(0.85) liveLocationImageView.image = Asset.Images.locationLiveCellIcon.image liveLocationImageView.tintColor = bannerViewData.iconTint @@ -197,6 +199,7 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat liveLocationStatusLabel.textColor = bannerViewData.titleColor liveLocationTimerLabel.text = bannerViewData.timeLeftString + liveLocationTimerLabel.textColor = Self.theme.colors.tertiaryContent liveLocationTimerLabel.isHidden = !bannerViewData.showTimer rightButton.setTitle(bannerViewData.rightButtonTitle, for: .normal) @@ -209,11 +212,10 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat } private func liveLocationBannerViewData(from viewState: TimelineLiveLocationViewState) -> LiveLocationBannerViewData { - let theme = ThemeService.shared().theme let iconTint: UIColor let title: String - var titleColor: UIColor = theme.roomCellLocalisationTextColor + var titleColor: UIColor = Self.theme.colors.primaryContent var placeholderImage: UIImage? var timeLeftString: String? var rightButtonTitle: String? @@ -223,42 +225,42 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat case .incoming(let liveLocationSharingStatus): switch liveLocationSharingStatus { case .starting: - iconTint = theme.roomCellLocalisationEndedColor + iconTint = Self.theme.roomCellLocalisationEndedColor title = VectorL10n.locationSharingLiveLoading - titleColor = theme.roomCellLocalisationEndedColor + titleColor = Self.theme.roomCellLocalisationEndedColor placeholderImage = Asset.Images.locationLiveCellLoadingImage.image case .started(let timeLeft): - iconTint = theme.roomCellLocalisationIconStartedColor + iconTint = Self.theme.roomCellLocalisationIconStartedColor title = VectorL10n.liveLocationSharingBannerTitle timeLeftString = generateTimerString(for: timeLeft, isIncomingLocation: true) case .stopped: - iconTint = theme.roomCellLocalisationEndedColor + iconTint = Self.theme.roomCellLocalisationEndedColor title = VectorL10n.liveLocationSharingEnded - titleColor = theme.roomCellLocalisationEndedColor + titleColor = Self.theme.roomCellLocalisationEndedColor placeholderImage = Asset.Images.locationLiveCellEndedImage.image } case .outgoing(let liveLocationSharingStatus): switch liveLocationSharingStatus { case .starting: - iconTint = theme.roomCellLocalisationEndedColor + iconTint = Self.theme.roomCellLocalisationEndedColor title = VectorL10n.locationSharingLiveLoading - titleColor = theme.roomCellLocalisationEndedColor + titleColor = Self.theme.roomCellLocalisationEndedColor placeholderImage = Asset.Images.locationLiveCellLoadingImage.image case .started(let timeLeft): - iconTint = theme.roomCellLocalisationIconStartedColor + iconTint = Self.theme.roomCellLocalisationIconStartedColor title = VectorL10n.liveLocationSharingBannerTitle timeLeftString = generateTimerString(for: timeLeft, isIncomingLocation: false) rightButtonTitle = VectorL10n.stop case .failure: - iconTint = theme.roomCellLocalisationErrorColor + iconTint = Self.theme.roomCellLocalisationErrorColor title = VectorL10n.locationSharingLiveError - titleColor = theme.roomCellLocalisationEndedColor + titleColor = Self.theme.roomCellLocalisationEndedColor rightButtonTitle = VectorL10n.retry rightButtonTag = .retrySharing case .stopped: - iconTint = theme.roomCellLocalisationEndedColor + iconTint = Self.theme.roomCellLocalisationEndedColor title = VectorL10n.liveLocationSharingEnded - titleColor = theme.roomCellLocalisationEndedColor + titleColor = Self.theme.roomCellLocalisationEndedColor placeholderImage = Asset.Images.locationLiveCellEndedImage.image } } diff --git a/Riot/Modules/Room/Location/RoomTimelineLocationView.xib b/Riot/Modules/Room/Location/RoomTimelineLocationView.xib index a149fcead..0954a24b6 100644 --- a/Riot/Modules/Room/Location/RoomTimelineLocationView.xib +++ b/Riot/Modules/Room/Location/RoomTimelineLocationView.xib @@ -40,9 +40,13 @@ - + + + + + @@ -90,12 +94,18 @@ + + + + + +

    !; z_9;`AhEDgD6&+tuP#KpUqK*f=<Yas#_O1djN=r_^xe)0&V$1wOI%}%U!_>O=kDs%q0riHfo{xo z<^GNx&Cfr@%B{R!gj-&Zl=L5J<_{k|uyDcl)57JOpM=jAOD%EnW*cl`KQx%CS7yc< z8HHWPUE3KUX)B705yYTNAc*(ou z`Nfx^fj>{$YmcQNK~sLLCi#t zXS2}2Yx8~VJGrtn*PHB)y|BwN_Ig0{GOKE2U3KfJ6U}mNoTNn`p{nN-Z+vq4>?XL> zoP6j{S=f8C)9;RxPsRGNf6T^5>vZotwWM8T=wx`_aH$uke6KD^@B={5W+mNTe55aa zP0Bby|L*?B!kQOKZ zv|~k>KSLE<$X)z#+TcgmzuGHiH7|~@c*r%--H0IRBqEznrDV#(R$rOlhi*i^WX9y} zynU$=lv8jnpxEqgtKY>gY>=O`1T_T%X&?kJ4RuXjjxlv+N5)HL`=z*p6O8xRvtL)1#uPeA-O|+RN_#)B zYAL00TG;wH#X~LiJ% zM{3d?#nj^U9zNBB+7g;uB>}2Qds^v2ha9}cDVl$cxVcO6qc=TWyb>}}RM@t6X}O?^ zZy*rzLx0Zf6g_h(sBf2dd(2xS=gcxpa4bmz0^6E}kh)CzbBZf0W0$`@PY}%S!{7Lp z@y5j2@BFq`R@XTnqEd(4_A3%Z$MW_`zA~>4x8(ne)=e-fTbjcEESr!+;r|mEdun;0zZfG@RCN+=4TpIu zK%}}mY~!2zA6vx&ISvII@GbTawOI4n-k&RK5>|4$Sd6v2&LQ-<0+8sm;r0#AOiXkE zA6|Gg9A}xLmtH^UmA&UY#?%;PmA*-{s4gm3YDY@~3#A(3ygJ$Dm*je{aLf z-#ErK@nmrZH~MV&86LOgf627Dl5e{;sC3+k==lN7IL=R?OqQbW&YFucpDr>!RWcV+ z_|9RR{(|3Ozb6ka^B8COa@dF+)oWm>2_Aa^dcWED3S87Uv%6d<{4Qn5)pwF2x2wNJ zA;tJ_#>^(G_?1L$RlnttKOaM^#G_Ik8>@VXAZflJmW}j)G^6hj0{dJ`?z8ePi<>`t zh4&0tI3gmx%aQk?K)XGwv(>&iLhha-Dlq5CuguNz5)@1%Z;k<~t}0 zUETy8-NEpPPb!SOqp)wl*nO%m>g4z5+%sv4`Pwu>`8}6zNNJBGZ|Hjvxn)TMAspSU zRZnBgO7HC2TP-~aYN6oHzs(kSE4a4_P-DRe_zG3>=bq-JQmRL7m*nRl-q+hgG%fkH zB)Qh3cmwo{VhKeX26c%#T8mZxKYw`2zua>BLgD#}G$N8AnfHrd`b2i-iZmshdaLg6 zgQLpb{13&1qCmr~o~ZSRM|f1$EFI5H+#I0NGYXxbx!U7a6*Uz5N5lN{hjKr!Sw1Z!T*+!d+3X5n<6Hi*sFEkJ=Ua z8d#(|v{Yg2v(b-&)ahc;?*CkNqFYRz%Y`7D z1%mR1p8R2u80?8&H4?hNXJ>Kl9${RSlyUqDmxBMSl)LzvSj*S_0C&epesb_D1ewK` zogJPsnJ2&Fi{=?ZEX7-C?j^*+SUZ;9IGB#+ic%?}0;}_1iV+%h#(49{c zI!per?$wXd^eBJ&2k_-&wN&eXaczlGZ{g#=u@YZW^@|I9xYQi+dOye;uh+O(+hy{; zri*{vu|d=iBA)>eaO^N!2CLLtQ7^k>%F7PSB4<<^c)=&nA zdJ4;+@mfPgicrnUTU%%pOzK^Sm7D4*# zJzyl-j?e^ZH%UcjSlk(pF-G)B;%%k4`DA|4VpEymGhl4-@|REiS)*L}4_i?s)}Wni zf1auW>50g}Qx@v>hDC)# zdy+rjfCV~!ZQ{pLvG{j-G5$~AfYRhYW(a3{g{mNXg1|xrF=x7Zn`ipmPTpy4(R1xf zjbEB}jxXu8UCeLBEO(vd`yIdjdu}=Tta7o{^~eZ*<-#-c^iZv?fa_N@ixU8Ups8xa zN&M%tp>o7833grMpFW0YNb~5LS5oSg2}E<^v5=hg#2#0WhJ-EN(T?TC{o88tZfR<1 zzRya+WZ;*V7pU5wcct+)En<0mJU@NJQ!6>h{{|d12S^#TgU0qUJA}%lC8YjN`ybAB zo)y_di zK$08ic#`~Fz(PS&@>F8%YzNX2+Y!hzf2@qm%6O2Fc4+~$;*j!dUzvUV2n$z$j+*6L zm%UzYQ^_t6M)gv})13J0kA$W6ZU+HGqG^@1|bAuPz~QCTS?K~8PAAqqd! zi+GyIGNsrXId?!!gfvjlg0&&|-d~Q+{zB(z=`>ibd|eQ!89I%n&l@_uIR89d&2ZK; z_^!?u{;pYkDTiZq^T)?`!h62Q+ptg7-MpJSpCtcY8v9v@j4VHhcbQucniy{Ust0*6 zM_F15w75!|wR{DiqcJ-(W>ld3aRfWzcaQjr*dl7416@Gj!(N`CiTna6>d0MIs!qji zS_f-TTvOcjk~;Id!RJ~!Io6~_5j~=DE`V zoQZ#Z*`kFvbI*?h=e9URb$Ka#y6J3~1Nywh9Yh2ALA_r(v|*kBUp0eQBlLB>guifg z*vUR`9ib-xq!wYk7bpdt#E^4KrS0Q?UU}ygNI12Zn|tyLUwBS^h+ju5>Z0NLdTOw1 znnTQJysR3T(&iF$au(n#C3%3f1{2k5JK1HgnW+oj{4vmiP|2HV<-~`lUa!B;1c59& zd!r;eFhvjrQbE-`9xq(zNpD(2dFlfvP%a6a(qusn%VDSLIKQa9Yg8ieF>h{t(t&WH z3RpG~GHYp|!6{Y}!P&K%p1fL`FaO>M+HyqmQYLfZ>Q^Fx%$pt6@s?1kWuG>?)I{c^ zE+`&yxHJvY%JHhmCoyaGk;+sH{-7$&N#zLW8y+bb?ocalvrEnx+C0X880+#8M6KTX zh`vm8!yxFXKk3<@KQvOWEixlF%_F@K=ebMGEOCHhf(VLaRVVnE%}zO$78ze|Kf*=W zr7j;r6%NK*osy-#C4TvXy~`c5M~)hnwn~^I=XVKqv*-psEr?GHyt}aK&t0 z{kgh+i!G@Z`=foJ9)QzB0w^ldn#`BgSgkt0t$flRUEwqvkg!t;a zU%o*tF@jyxdV~yojBM}V0-N0Qt>%(k4C4Z&wfMhL|^y11xU$t}u zDF4F_oWwW_Z`mLC@Lj3OTh`=eJ_xIitF~hNx=tUbr~UOcJU{{TDlXh)Md)-r0ws(d zbqH=I#`W2~4`Wk)dQM)?qjT6RAARy}Uy|*p0-py=`}6Cjt8Osq{BAj_rQ}Y7{A%#2 z-XyF_{I|xwsgGp7%dG4kyLqt?IR>8?I3b7fSyMAJbxcoEF)O`h$H59&V%2LMvi zV4dqhKSYb>tI&~hrC&xbx`&f*re(2wq-NLNqCU&dS8;3Va{A$x2zuY8VY7KkVsFvA z)L8#>beUI#rP!^=F2(FY`p0)DtyF*gp2TJ_RYPi4k_PJZnR##V&mXj3WTa&_ zuLkFFwXtguboGKdHY`$K=juy8?@Q~WFPM%7E*F2U)1Xhko6)VNf*K0j)XHHHBn3T( zTFj>oT~H)}0w%2Uk*T9&g*^Fy`59RngM4tYpmvb`ZQ+6Tf8pcYSXFA5>(9}i z#qHdZmLAQdoASTOq$WTZ*sPfOuTG)HwT3(1mDIzJH!3HII~p43mPc*f2YT+C8US!Q ziyaBA;%v8?FNHa~cgp)-k%Hy?8XKRr)e3V(>r+46&)S%JP7hy3_TzXPktN6e*~3fQ zir?69m3RCsTP}m0oa=8xM-TG44#L-CLCF=?2KO8Ku&zo$FGXczssW2qM91wmVtY)( z=LwV}pd$|*AEN?Kc6`R;eKEB`nQF?5bwLC{SRJVQO`;ArgrYNE!~oU_6bZEvm) z-2wqj_T|Ngu3AQp9qt3)!%dX)wX z!eUK^<;+4rR!9s;^{Z3OsENWC=CZjXSsV}fDAJo?jQDPZR;lo9xGhMqd_rQg(c10j>~ zdP+1dxGIef^*eNBYoGBgOF=7Vs7@Td?QypZC&}Y9tVb{WYQ5s~xy6Nt<6@*j@~O9B za&=I(l3))!1ttBOUuTt22c5+TA;*7}1zW|xgA0A> z?dr8rCzhoiTq^r~?=R!t=_ZS|+0&!uvw9uypJyPl)eHZeqVe$9cEhmld$OIw$ZOx~ ze9apMQ~?BS5k?5*4S1!-%S&lmaX-9BR-{V7Lf(Azrh3R65C0~_(x-76s-yM0F4A#V z2;tbwLUre}!iJaLfCn$}6Rk2mpRLee9z3N$Wa{V`Pj%9poIO(@K|=(Irjh(>0a4t{ zCbhcfMOzSvX_NXwVvzZNLjNqPDck+a$bK60PZQ=|(`;0kF0nfmF~5#@HUnVObc}!W z>JOiXO5UGxHIffp(TEK{>RG8VQ=8;H{lIdq+0KUeCEU>Q|K~vOe#G|u*i}P$gg#{I z5gHlQMQ&{`yuJ4d?YScOp3F|zkPJSad;mC5=DDEYE%>Iii`z=c_;?)>^o3<#Wm0V7K3ybG*hLO$lJ|i*Iz_1R>6X&AS0ms73{{s-F?>}DED-O{&KVC zvn;NDOzwoWRvy+>8grClHHFw3y*_i7TM+JLZ4q$-l?s`jo(C**=V!R~zcn&nyb-y- zR|E|+vGP%kv^eMs(!H83d!{N_mMbO|^eRb4zxV8oXESSkYhiOQr$k0)_y(_^o?y(& zy{`Zt_lD}3$AGT}W2*19 zjp5uQJu`-FQX7yvTKXP0VQ>h%-<(vBiJC)MZ zKfYwDb(IOlMLiE@}V294rNPL zzDRGXI}y`T^sn+(WhF>n7e9+l)foru_=UQUaE)j);pqMFV_im`D0sam^`13y> zYLwqC7@zr(M`OqUW>46A;gU0;*z-D5hYUBP?pF~g-J-+SMccydn6IzcftH_~8E0yg z#`rN1A8?P)TMBY$40GTJB-cQ>?ed%DzV|8ssyiPnsVLKNIqsD>hm>n+cSj#x8o>_LsYWzi3|z;&ckKbt zpIb(%1W~Jl%$oTwT4xIbstE4Pv;2K)?DIo`sNy7dYr#dn`=ydMuh&;05x>)9QrgVZoo~j2cRq{MEG~KN4+TTpKdxL zY1-t^>y!u4}9!B_1` zRnO`dZP}o?op%KEaZU6QvHSLcP$tUnNcKXd7qCOHj3oRQ^~-gw*v3hXw)?eyqf*_x z1my{6b?^0Z zd*)gp!P!9;`KGvQg2qdXjM&nkx4VKdQaSyVxjNM{v>~}KDHa#;p-1X`G9sIf1+Cse zl)YBjq4n;f^&AGd zfT$Q(KEE>VQS1z}1Fq44j=klM%O+VKxUAfH`!?0iGfo{qEIe1~dg8mT^ikKONcjUJ zv}m}-SEDloKj-RWk81}$8d!ku(5uV+;2yPF%ja;$3v@4>k}DZlubwjtyVcw;kl#H2 zO7mUZBqc1h@($7&BXli0-RBwool!fvR$cz0X6G6YH?kdidpfyui3W9;Ip$9sJEg0X zK3e*$S!RvCR}cH0ZdGXQ{f;mTM^jWmKTDl9>^(KjH$8>9)GDHDO-JEtI7fk@o&1Z; zi&cuScQ--qc`V)PgfXkRREpNU0a4*|qImNoeB(vYPquP>UG>_6t1~vNKkqfCJ0N~A z-am!juFW^7o%|IoH3#>je#udf+=+EC7+?+s7*+7PZh;brrnWJD zRcpg-HNnS-epjtgZklQTefOPf_%`xoGCmJWpufM$Kxu_^ss;|7{KjVGU#eY9d`~3A z78eibTsl;>8T6CcfA?1fj?O{zHTP~5?341B5g1T;Mi~}08N}oLVbu?1ke7U2w|E;x zhjM-xFT@^DMY7gn3D8WNtqxn#*=mR?^5Gb{mr|zJENYnU72lt7whYVy>pk<6k>KQZUxd*KCpw^foN zJgmPu^epcn_YCH%lV&ArY=KPZ+SQh*q!R9^Mlt`Ku_q7Mf((eBbGNgSIuG_`D$qCK z_dGxp;zMv5?F$)XE%C0iF%QE11eWX+mV)}m+RTSQADKe7m-I3#-0%H$r*!JGYm55u zv(A!+e$~3wsXz)nR3z@|+2LbLrFqzk^fo-JMV(njySl)tDZ7dz{@$8k~;8E!Zce!m4_unS$Q$!e^-UWZ2zce!k@hC~UU2L1Uf~~d-?XtDU;wjtCv9!?4Uu+{ePp`? zJXxJ5mNkBIu(r##Sa7IffGn&Y2)Ik#8hNItCd%pbJk7gb(e}Uv7dRtncW+>GBIxfZ zib#)x)B^RvcESF)?fA${%=F8fVB_%GT}>W2X*km&tZkKjq(zh4sVYF%66Xs&l-u_v zvZY5D%h6U$Hhgk{^;1h8<|c*g!arcFAKSa5O<^JLd44KUK{n@KWmoGYHZMEV=gPS{ z&4(hLfj%YDqr-sc0zXp9-#p=Ui(If`eS`}G0 zy4Ss>_WHt=Eux<#lh3LETCZ&N)!yoNpfUFJxpXw;uN+nI5GHsu zE^iQPGO(y+$w;u7Jua-I%{M)a=FH5o;Xu_LAfF>DBiqhgwyEcf zH<*r(gv+O+s=k#1F*yLAlt1fQk7#-3Z(}BT*JcPRTi(NwpjYOflDxQ3JRF^=Tj>K$ znKfgJnn`3pMaAChd<(Z&D%gZ45+f>yi!xGyy4ytTJKToW>=u39t&-s#oH0HgA_+A} zhe}^si7G<+SFe7gW&*RFk!RJe*H98vI#n`U-#%uGmuCxVuY#j84s4(W3xnM!-ikq- zRM~}+mTtf5Rls*65QJf=jW=-3>jmLI18AY?CNX+_a%2Rq=#TEYU39PLiO(>~YESL&^u(j6@Vq^LV3D zTlct{?<5K}l_eTCXLxuV;5Cw8eihun$Mi zF~N?OLRcc;5b@~%*KNBcbu!+x2I=W_T~Lq4M~YqP9v}9I3f4B*;6FW*r|8Jbys7&N z4mhi9NxCceT2NJ7r-4rtJAv(!^=@n>vCQS37prd-}k0XEZ|} z>y)HPE#wj3l4NAz<2Bibx3vWhnTE-o>9(Qzv9p)GsG@JnVJ5CVm+z!akP!deaZ-Um zeN)-1oOH>UU6W?kNOv@qfJP;C7UGW5gQJOVJK2-Ci^7!p87Ch}Lk@hBBKvS!##Uu# z#xdVPpZGj;7;7J>X+R_B1Y0y505#7@gHSD{`Sl&In4;X({mC6_ zXnR>!_G$U7ZdGzitGWvgBpV$@rXo0y4-Y5=3S*zR0X8CK&Ex|DgqWw3igtG&g4=_amHL(fdm#q}=tDJ##@4Z>$fn$;TRBzEj;1A6$~H*B_pLQ!jCyIFd|Q(408FDkflu1Q$4^C{noi=Aa{tXeyVaMg z2HZOUhw~EYy(ybvR~JQQUKPkgoUtA^3Z)P$t!D63MD#%oZp;lBA?4h&H+#5j=7zt_ z)rM457&`y;D&8?$(q;{5H{@Pd!oy=VJfU2sHIH+sKq&x_IAm?{*~m;P(3|y2f2B}p zg4ae-o;~xlE*Rm9IQN)zfZLv!ZBfzkVqG4RoiuR-)d;IZ5Na39NcKg>djooDtZ`93 znScK%wydp2&O-PGX*v^kN$2J?l@h`3|(_CzI`jQT(~jh z+yfjW%InWj67?G|IY)GI%K70}ZWR&F!7th)fBw!y){Pf#X`BK@uz1wqS4C)1id$<5 z<)(~OIU4l{Cff!%1-pGaBEj;>MBA!2yjpXI#$@A|Q|^<9)r#IL{lfWo{j<_u;mz8AWWnSR?G1<#;|qnZA8zyEa^Xvky8dk#y@ZO2K2N;eW-&1(6#)@ z-38m72?gLWzoG!=8klO=a_xBQN@=Hz4g>P#MMi8U~V0xxRHMi4$Hg}|@bIiH* z7pxpS1^n}D#x2#r#8Uy&I_JLvKsld{#5e{jFmbL52J)44l%Wb|a6WGO6Zn|1$I*h} zNoDmtk(u6-JKMFP4>=TyKJkBUK8DzC1Oc!>7YZ?P0X zt}Wl1h+9i9LwMGzu+4nHba|?;@rwZw;2}``9zCa2!iumir^Blc$@7k)_{T8+SsbYU z;J_XHpRMa_KNqZS(&d+q#&)scs?OIKW1?6uGlpGB$HAW=0RRsH1wr5`IDdO+{{70- zhEe-s9f-LaNlgi*o2_`T3gD-co5_GTRTQDFypd})?`mN1FltxvW&6_C%A2Y1wHm<@ z2%#$4%*ideeIaWHxVU+=gHTENZ02b5crLsufqD%_sN33agyP+U5@NeS;IxTlOX{pE zx;$R8NPBsDRM_)rCIDXYZlg_Ak)_0@8vc5iP#va5c9cYr^LblX!(^sexolN&&Ohr0 z1`RLgU5uCm8gJV6eS84FUCQs%#!rb&D$Vn# zVV0{ZC0G-)Z61GoAl;gqa~Q%Jh@I%op^i*_^7`=h z|4~GOOJ9C@ShE3ZA8@fO?-K-&Y&{E=KE!5uCJJhs7*D9`Yy?(CzS||x_?T9>3KfX2y?^;g^e? z&xB_p^zshoYn}bn0q0I^ZZ6CE`&#HJ1_3`2(G7-}JtTC~CeBDZRHA{TSg^N42fkYo zVm_3%w6iDcctx2Y8RlM$on&y6h9qCGl$7k$%SuTUZ8*QV8WZ^Jw&@{Dp5?qeh`+8k zoC=C)l=YLryO#Qz{TKYy?^eou-wU@E+t+rGPp29_oOC2tBkMs-$IhbOi;hV$@yt4f z$;Oh1;K21eSK)=BEoF?&w)8j zm3KhGU(0&s{3y!O>B1jZt!>}s&vncDbNNx+BpIkx&z%Z4#Gp#5eFaEF{0RvR9S>&g zh81y}qpN-C-KH$U9%-Ig4=A?~D-MI=vBsmd$;}l#b^`S1_=X)A=m>d{$(i?FXSAHV zz{EnVX_NI)PQ`{hMm5y<1#YR@1G5D0dRa~nYZ)INJ7ksuSr7?yQc%coqOR(szV|-8 zEJ6%n=8u}>6u4d1ts4zv^jme4?I=U|>?v^_(0jzrp`|HRl%^#C`p!}S;iAcuJzQiO zbf!(iC*6~yl-54GNOzFLps|EMsGEyx$iO0G$f;39q{rs*gfJSMCkonm+wmkNvf(>q z-8SH)S|WGbAm#T*B_gwNVvgDOqd#_`$(;On|6kRDf?9&``1P<)9Bl}q>twbdFx%2k zNVX^Rx8TipMvz8+-0(}NCByFp{<2N@{^ZvnL!qEall6#t{>;Q}FRC$xH{~*a1RS4& zDU{HT$DWY^2yK=bLl1boPHDw4|I1J+9f+5IY%yQeRzwgYH1m#hvn1IZ?Kv=%vTY@Kl0H7{@wW^@i7qpv%yCZwtSK^Bk(djT|t-%ETN;}~NHuYO`tw&fqK zfpb>*)x$IaTCWAf%Gz;pxu6C(4ms&w#n+F}q4?{{(nu_y0-(26sIxz|>y7>{7F= zqBMN38`4#2_cfbdbA~#hNpUOWW{f{x7`%;pweY5%wU;D`C`KbitLSH*FGG7>%*30Nv6QEh3!}e QRPB(BrNg=BXFYHKAFv(i^#A|> diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_placeholder_background_image.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Location/location_placeholder_background_image.imageset/Contents.json new file mode 100644 index 000000000..2753cacb9 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Location/location_placeholder_background_image.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "location_placeholder_background_image.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "location_placeholder_background_image@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "location_placeholder_background_image@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_placeholder_background_image.imageset/location_placeholder_background_image.png b/Riot/Assets/Images.xcassets/Room/Location/location_placeholder_background_image.imageset/location_placeholder_background_image.png new file mode 100644 index 0000000000000000000000000000000000000000..b161191b7e11aacb55b7dc1ab88206348094d90b GIT binary patch literal 3852 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 literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_placeholder_background_image.imageset/location_placeholder_background_image@2x.png b/Riot/Assets/Images.xcassets/Room/Location/location_placeholder_background_image.imageset/location_placeholder_background_image@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..304c25f56bc2690d8af447718528b7e8a94cf2f2 GIT binary patch literal 10738 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 aAEmkzF1lWlW85EcLN=E6r>GW}NdE^S;Qt%| literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_placeholder_background_image.imageset/location_placeholder_background_image@3x.png b/Riot/Assets/Images.xcassets/Room/Location/location_placeholder_background_image.imageset/location_placeholder_background_image@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..1bfa2e44153b09a0721bcf6f09a50d739f8b4d06 GIT binary patch literal 20687 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 literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker.png b/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker.png index eedadad5e75f36dd5acc6168bcce7c67fa51144e..4f1a4b8fbf27177aa28dab5b446b5c138e60c7dd 100644 GIT binary patch delta 808 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 ALI3~& diff --git a/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker@3x.png b/Riot/Assets/Images.xcassets/Room/Location/location_user_marker.imageset/location_user_marker@3x.png index ff8ce3aad4d32c482789678ebfc0c714ffd28211..e5a28f6e6a6d315ce819ee933ff6920f8515fe97 100644 GIT binary patch delta 2225 zcmV;i2u}CV6w47IiBL{Q4GJ0x0000DNk~Le0001f0001o2nGNE0MNeeS&<<$e+Xbn zL_t(|0qvb#avQ}K$4~djms=H^tE_-A8;I|{3y2g0uTXabF>HYN77oP=mU*GdP7!AV zkgx&56IDPWn+?R-02Uh{0>>$cG<`VT8WBm>$BaB5J>BP*aw(D%f9T);(`U}pZ{%LamI2q~@siVae>4!Iz@An*hgJ{5pRz2F}we>Ar{P*Ex;{OS(} z4HDozDaCbiqwQ0Q;w8TuvUguX71RP+Q`^koQF$S|fi6*V_Wmy9K<2`e=ro_>AuySMO0@D2Khrrr7QF>b<_L+c-uFAo7~`}fU{mrkOW7T)yF%_SRC7E}#wF1yTYDdD*y zXANDd@cnz43SVmB&5~x-=Zc&?bOI7TZEZhw0Y^7nfJL{7P^o_E0#7Q(4;D~}uYj=+ zDYnwIbQ25D^SbqmZ?5BGw=)%FQKC1~@$B~MGq09E z)A8)~>SH#c&idT67uoIZFw6B*9nWre2Wp*9y{^CDcy_yUe@wluzu@>t^%}sqM7`gC zd13q7lu$2nS-{Vfhmi6@xE~Wf-m(({;~L@QcC0bs&7nh#n+SYw^yzr07jPXIClNx7 zh5NsS=P>U)_0e$u%JCDSfN_u!jYmSg;4tqvaztZOz~Q2eKTt8}sxIaj$460A>M77n zaYTs5^+tFEf7JlS3sCi&`q2n)4*6%iV5s#Vxhwg=f2KfUt;Lo0{)SZI2{7K!d)Z%? zqA&PlD$iJX?X_zXfy2CSsHjJd7Z?KL6;!tmsh9(A0OJ+e6(8@d^R*L9s%P8Le|VJEGxddI%PHLJ)2~8c9AZ4o zYc4Fknj9HS2}Rb;bYggfvGAp-L9Ft zlO_=T4}hs6bKS0)D}0wm45By$rb-|zaJy#V-nm*3=(+wGFckqii`=eRDE#0Kh4_D9 zf2sgg=|pRh+cg3bviA7kF@4!4Ft%bs{MqJCqP#5eHt9IgdFP;2i#XZhBF#J#iT&ev)FQ|c2j23bDx9jmMXPiw)8)K%8h!s{$QkkJj` z`Q8HXv?rs@ZnKrTjlR?wIHs_p;D0d4eOCNruYxxR*lf7~v(bXPl zu=HMtOPzXkzPWQq&gU*(vBaW(e5v<6aRFx@t3MpHgoIsS@+K3}HJJ#nVexY(Gto7f z39n)Cb0bsHHJJ*pVevC3bI~=K3$KaA|1__!?-8UqY@ffqa}P2mH(^NphLHFre|?K} zV3y%mT`RfFCEGl5b!5H$c^IkSF^iuH99HC(EzTq%*VC(Yg)fjR|A`wrT;*GgX}ARP z#eee@T~pW`y!L2+iyl5@_s`@RM7VwS&h0|>E*6@6nb05AcIms`1Wx>hkjdewAM`)S z+vTH_^5(Pq4SMS9(qwxpZ7)3we{!AUXQ3B#nAYxKkDiZne>|6;zgdRpn$p9qXW_q; zus#iI-e|`MfDxQx+g@JR?i8s@fe{}>9c-_)LVBBqvi$n3yHg`{>a$!^te*zi+@6wNc zod&~3{BqI1hq)3Nod1i*RS|oXN+(hEcB>vms|^IDBf+(JAY6rfcoqx&R3Gym1kaCc z4dY30DtpeaDTF`g-83nhQVKYEF=z@!H^})Z{n9s<}QO5UWx8CdhP$}kNbDQQ~m-tzS$f2y;U^;0000<2SrXqu0mjfqp?JE delta 2617 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 + + @@ -8,56 +10,42 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - - - - - + + - - - - + + + + + + @@ -71,6 +59,6 @@ - + diff --git a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift index 21883ca76..c8f680e57 100644 --- a/Riot/Modules/Room/Location/RoomTimelineLocationView.swift +++ b/Riot/Modules/Room/Location/RoomTimelineLocationView.swift @@ -31,7 +31,7 @@ struct RoomTimelineLocationViewData { } struct LiveLocationBannerViewData { - let placeholderImage: UIImage? + let placeholderIcon: UIImage? let iconTint: UIColor let title: String let titleColor: UIColor @@ -48,7 +48,7 @@ struct LiveLocationBannerViewData { } var showPlaceholderImage: Bool { - return placeholderImage != nil + return placeholderIcon != nil } } @@ -96,7 +96,8 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat @IBOutlet private var attributionLabel: UILabel! // MARK: - Live Location - @IBOutlet private var placeholderImage: UIImageView! + @IBOutlet private var placeholderBackground: UIImageView! + @IBOutlet private var placeholderIcon: UIImageView! @IBOutlet private var liveLocationContainerView: UIView! @IBOutlet private var liveLocationImageView: UIImageView! @IBOutlet private var liveLocationStatusLabel: UILabel! @@ -208,8 +209,10 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat rightButton.isHidden = !bannerViewData.showRightButton rightButton.tag = bannerViewData.rightButtonTag.rawValue - placeholderImage.image = bannerViewData.placeholderImage - placeholderImage.isHidden = !bannerViewData.showPlaceholderImage + placeholderBackground.isHidden = !bannerViewData.showPlaceholderImage + placeholderIcon.image = bannerViewData.placeholderIcon + placeholderIcon.isHidden = !bannerViewData.showPlaceholderImage + placeholderBackground.isHidden = !bannerViewData.showPlaceholderImage mapView.isHidden = bannerViewData.showPlaceholderImage } @@ -218,7 +221,7 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat let iconTint: UIColor let title: String var titleColor: UIColor = theme.colors.primaryContent - var placeholderImage: UIImage? + var placeholderIcon: UIImage? var timeLeftString: String? var rightButtonTitle: String? var rightButtonTag: RightButtonTag = .stopSharing @@ -230,7 +233,7 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat iconTint = theme.colors.tertiaryContent title = VectorL10n.locationSharingLiveLoading titleColor = theme.colors.tertiaryContent - placeholderImage = Asset.Images.locationLiveCellLoadingImage.image + placeholderIcon = Asset.Images.locationLiveCellLoadingIcon.image case .started(let timeLeft): iconTint = theme.roomCellLocalisationIconStartedColor title = VectorL10n.liveLocationSharingBannerTitle @@ -239,7 +242,7 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat iconTint = theme.colors.tertiaryContent title = VectorL10n.liveLocationSharingEnded titleColor = theme.colors.tertiaryContent - placeholderImage = Asset.Images.locationLiveCellEndedImage.image + placeholderIcon = Asset.Images.locationLiveCellEndedIcon.image } case .outgoing(let liveLocationSharingStatus): switch liveLocationSharingStatus { @@ -247,7 +250,7 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat iconTint = theme.colors.tertiaryContent title = VectorL10n.locationSharingLiveLoading titleColor = theme.colors.tertiaryContent - placeholderImage = Asset.Images.locationLiveCellLoadingImage.image + placeholderIcon = Asset.Images.locationLiveCellLoadingIcon.image case .started(let timeLeft): iconTint = theme.roomCellLocalisationIconStartedColor title = VectorL10n.liveLocationSharingBannerTitle @@ -262,11 +265,11 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat iconTint = theme.colors.tertiaryContent title = VectorL10n.liveLocationSharingEnded titleColor = theme.colors.tertiaryContent - placeholderImage = Asset.Images.locationLiveCellEndedImage.image + placeholderIcon = Asset.Images.locationLiveCellEndedIcon.image } } - return LiveLocationBannerViewData(placeholderImage: placeholderImage, iconTint: iconTint, title: title, titleColor: titleColor, timeLeftString: timeLeftString, rightButtonTitle: rightButtonTitle, rightButtonTag: rightButtonTag) + return LiveLocationBannerViewData(placeholderIcon: placeholderIcon, iconTint: iconTint, title: title, titleColor: titleColor, timeLeftString: timeLeftString, rightButtonTitle: rightButtonTitle, rightButtonTag: rightButtonTag) } private func generateTimerString(for timestamp: Double, @@ -311,6 +314,7 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat descriptionIcon.tintColor = theme.colors.accent attributionLabel.textColor = theme.colors.accent layer.borderColor = theme.colors.quinaryContent.cgColor + self.theme = theme } // MARK: - MGLMapViewDelegate diff --git a/Riot/Modules/Room/Location/RoomTimelineLocationView.xib b/Riot/Modules/Room/Location/RoomTimelineLocationView.xib index d014a83ba..79e8d7097 100644 --- a/Riot/Modules/Room/Location/RoomTimelineLocationView.xib +++ b/Riot/Modules/Room/Location/RoomTimelineLocationView.xib @@ -14,9 +14,16 @@ -