5720: Rework some location sharing related view

This commit is contained in:
MaximeE
2022-03-23 15:48:19 +01:00
parent 8e3107095f
commit 7f140388a1
35 changed files with 218 additions and 126 deletions

View File

@@ -47,4 +47,10 @@ public struct ColorValues: Colors {
public let background: UIColor
public let namesAndAvatars: [UIColor]
// MARK: - Others colors
public let white: UIColor
public let purple: UIColor
}

View File

@@ -68,4 +68,11 @@ public protocol Colors {
/// - Avatars default states that include first name letter
var namesAndAvatars: [ColorType] { get }
// MARK: - Others colors
/// White
var white: ColorType { get }
/// Purple
var purple: ColorType { get }
}

View File

@@ -49,6 +49,12 @@ public struct ColorSwiftUI: Colors {
public let namesAndAvatars: [Color]
// MARK: - Others colors
public let white: Color
public let purple: Color
init(values: ColorValues) {
accent = Color(values.accent)
alert = Color(values.alert)
@@ -63,5 +69,7 @@ public struct ColorSwiftUI: Colors {
navigation = Color(values.navigation)
background = Color(values.background)
namesAndAvatars = values.namesAndAvatars.map({ Color($0) })
white = Color(values.white)
purple = Color(values.purple)
}
}

View File

@@ -48,6 +48,12 @@ import UIKit
public let namesAndAvatars: [UIColor]
// MARK: - Others colors
public let white: UIColor
public let purple: UIColor
init(values: ColorValues) {
accent = values.accent
alert = values.alert
@@ -62,6 +68,8 @@ import UIKit
navigation = values.navigation
background = values.background
namesAndAvatars = values.namesAndAvatars
white = values.white
purple = values.purple
}
}

View File

@@ -42,7 +42,9 @@ public class DarkColors {
UIColor(rgb:0x2DC2C5),
UIColor(rgb:0x5C56F5),
UIColor(rgb:0x74D12C)
]
],
white: UIColor(rgb: 0xFFFFFF),
purple: UIColor(rgb: 0x5C56F5)
)
public static var uiKit = ColorsUIKit(values: values)

View File

@@ -43,7 +43,9 @@ public class LightColors {
UIColor(rgb:0x2DC2C5),
UIColor(rgb:0x5C56F5),
UIColor(rgb:0x74D12C)
]
],
white: UIColor(rgb: 0xFFFFFF),
purple: UIColor(rgb: 0x5C56F5)
)
public static var uiKit = ColorsUIKit(values: values)

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "location_live_icon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "location_live_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "location_live_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "location_pin_icon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "location_pin_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "location_pin_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -19,5 +19,8 @@
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@@ -172,7 +172,9 @@ internal class Asset: NSObject {
internal static let voiceCallHangonIcon = ImageAsset(name: "voice_call_hangon_icon")
internal static let voiceCallHangupIcon = ImageAsset(name: "voice_call_hangup_icon")
internal static let liveLocationIcon = ImageAsset(name: "live_location_icon")
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")
internal static let locationShareIcon = ImageAsset(name: "location_share_icon")
internal static let locationUserMarker = ImageAsset(name: "location_user_marker")
internal static let pollCheckboxDefault = ImageAsset(name: "poll_checkbox_default")

View File

@@ -20,23 +20,17 @@ import Mapbox
class LocationMarkerView: MGLAnnotationView, NibLoadable {
@IBOutlet private var markerBackground: UIImageView!
@IBOutlet private var backgroundImageView: UIImageView!
@IBOutlet private var avatarView: UserAvatarView!
private static var usernameColorGenerator = UserNameColorGenerator()
private let theme: Theme = ThemeService.shared().theme
override func awakeFromNib() {
super.awakeFromNib()
translatesAutoresizingMaskIntoConstraints = false
}
func setAvatarData(_ avatarData: AvatarViewDataProtocol) {
Self.usernameColorGenerator.defaultColor = theme.colors.primaryContent
Self.usernameColorGenerator.userNameColors = theme.colors.namesAndAvatars
let image = Asset.Images.locationUserMarker.image.withRenderingMode(.alwaysTemplate)
markerBackground.image = image
markerBackground.tintColor = Self.usernameColorGenerator.color(from: avatarData.matrixItemId)
func setAvatarData(_ avatarData: AvatarViewDataProtocol, avatarBackgroundColor: UIColor) {
backgroundImageView.image = Asset.Images.locationUserMarker.image
backgroundImageView.tintColor = avatarBackgroundColor
avatarView.fill(with: avatarData)
}
}

View File

@@ -1,8 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
@@ -65,7 +63,7 @@
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<connections>
<outlet property="avatarView" destination="qut-wn-BX3" id="wHA-bz-A2y"/>
<outlet property="markerBackground" destination="ldO-kc-R5W" id="52a-Fs-iu7"/>
<outlet property="backgroundImageView" destination="ldO-kc-R5W" id="52a-Fs-iu7"/>
</connections>
<point key="canvasLocation" x="58.695652173913047" y="4.6875"/>
</view>

View File

@@ -39,6 +39,7 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat
private var mapView: MGLMapView!
private var annotationView: LocationMarkerView?
private static var usernameColorGenerator = UserNameColorGenerator()
// MARK: Public
@@ -82,7 +83,8 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat
annotationView = LocationMarkerView.loadFromNib()
if let userAvatarData = userAvatarData {
annotationView?.setAvatarData(userAvatarData)
let avatarBackgroundColor = Self.usernameColorGenerator.color(from: userAvatarData.matrixItemId)
annotationView?.setAvatarData(userAvatarData, avatarBackgroundColor: avatarBackgroundColor)
}
if let annotations = mapView.annotations {
@@ -99,6 +101,7 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat
// MARK: - Themable
func update(theme: Theme) {
Self.usernameColorGenerator.update(theme: theme)
descriptionLabel.textColor = theme.colors.primaryContent
descriptionLabel.font = theme.fonts.footnote
descriptionIcon.tintColor = theme.colors.accent

View File

@@ -94,8 +94,7 @@ extension ThreadTableViewCell: Themable {
func update(theme: Theme) {
self.theme = theme
Self.usernameColorGenerator.defaultColor = theme.colors.primaryContent
Self.usernameColorGenerator.userNameColors = theme.colors.namesAndAvatars
Self.usernameColorGenerator.update(theme: theme)
updateRootMessageSenderColor()
rootMessageAvatarView.backgroundColor = .clear
if let attributedText = rootMessageContentLabel.attributedText {

View File

@@ -48,3 +48,12 @@ final class UserNameColorGenerator: NSObject {
return self.userNameColors[senderNameColorIndex]
}
}
// MARK: - Themable
extension UserNameColorGenerator: Themable {
func update(theme: Theme) {
self.defaultColor = theme.colors.primaryContent
self.userNameColors = theme.colors.namesAndAvatars
}
}

View File

@@ -27,7 +27,14 @@ struct AvatarImage: View {
var mxContentUri: String?
var matrixItemId: String
var displayName: String?
var size: AvatarSize
var size: AvatarSize?
var sizeValue: CGFloat? {
guard let size = size else {
return nil
}
return CGFloat(size.rawValue)
}
var body: some View {
Group {
@@ -42,7 +49,7 @@ struct AvatarImage: View {
.resizable()
}
}
.frame(width: CGFloat(size.rawValue), height: CGFloat(size.rawValue))
.frame(width: sizeValue, height: sizeValue)
.clipShape(Circle())
.onAppear {
viewModel.inject(dependencies: dependencies)
@@ -59,7 +66,7 @@ struct AvatarImage: View {
@available(iOS 14.0, *)
extension AvatarImage {
init(avatarData: AvatarInputProtocol, size: AvatarSize) {
init(avatarData: AvatarInputProtocol, size: AvatarSize?) {
self.init(
mxContentUri: avatarData.mxContentUri,
matrixItemId: avatarData.matrixItemId,

View File

@@ -40,7 +40,7 @@ class AvatarViewModel: InjectableObject, ObservableObject {
matrixItemId: String,
displayName: String?,
colorCount: Int,
avatarSize: AvatarSize) {
avatarSize: AvatarSize?) {
let placeholderViewModel = PlaceholderAvatarViewModel(displayName: displayName,
matrixItemId: matrixItemId,
@@ -52,7 +52,7 @@ class AvatarViewModel: InjectableObject, ObservableObject {
return
}
avatarService.avatarImage(mxContentUri: mxContentUri, avatarSize: avatarSize)
avatarService.avatarImage(mxContentUri: mxContentUri, avatarSize: avatarSize ?? .large)
.sink { completion in
guard case let .failure(error) = completion else { return }
UILog.error("[AvatarService] Failed to retrieve avatar: \(error)")

View File

@@ -23,7 +23,7 @@ extension ThemeSwiftUI {
/// Get the stable display name color based on userId.
/// - Parameter userId: The user id used to hash.
/// - Returns: The SwiftUI color for the associated userId.
func displayNameColor(for userId: String) -> Color {
func displayUserColor(for userId: String) -> Color {
let senderNameColorIndex = Int(userId.vc_hashCode % Int32(colors.namesAndAvatars.count))
return colors.namesAndAvatars[senderNameColorIndex]
}

View File

@@ -0,0 +1,37 @@
//
// 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 BorderModifier<Shape: InsettableShape>: ViewModifier {
var color: Color
var borderWidth: CGFloat
var shape: Shape
func body(content: Content) -> some View {
content
.overlay(shape.stroke(color, lineWidth: borderWidth))
}
}
@available(iOS 14.0, *)
extension View {
func shapedBorder<Shape: InsettableShape>(color: Color, borderWidth: CGFloat, shape: Shape) -> some View {
modifier(BorderModifier(color: color, borderWidth: borderWidth, shape: shape))
}
}

View File

@@ -49,7 +49,8 @@ final class LocationSharingCoordinator: Coordinator, Presentable {
let viewModel = LocationSharingViewModel(mapStyleURL: BuildSettings.tileServerMapStyleURL,
avatarData: parameters.avatarData,
location: parameters.location)
location: parameters.location,
isLiveLocationSharingEnabled: BuildSettings.liveLocationSharingEnabled)
let view = LocationSharingView(context: viewModel.context)
.addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager))

View File

@@ -62,6 +62,9 @@ struct LocationSharingViewState: BindableState {
/// True to indicate to show and follow current user location
var showsUserLocation: Bool = false
/// Used to hide live location sharing features until is finished
var isLiveLocationSharingEnabled: Bool
var shareButtonVisible: Bool {
return self.displayExistingLocation == false
}

View File

@@ -37,7 +37,8 @@ enum MockLocationSharingScreenState: MockScreenState, CaseIterable {
let mapStyleURL = URL(string: "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx")!
let viewModel = LocationSharingViewModel(mapStyleURL: mapStyleURL,
avatarData: AvatarInput(mxContentUri: "", matrixItemId: "alice:matrix.org", displayName: "Alice"),
location: location)
location: location,
isLiveLocationSharingEnabled: true)
return ([viewModel],
AnyView(LocationSharingView(context: viewModel.context)
.addDependency(MockAvatarService.example)))

View File

@@ -35,7 +35,7 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie
// MARK: - Setup
init(mapStyleURL: URL, avatarData: AvatarInputProtocol, location: CLLocationCoordinate2D? = nil) {
init(mapStyleURL: URL, avatarData: AvatarInputProtocol, location: CLLocationCoordinate2D? = nil, isLiveLocationSharingEnabled: Bool) {
var userAnnotation: UserLocationAnnotation?
var annotations: [UserLocationAnnotation] = []
@@ -60,7 +60,8 @@ class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingVie
userAnnotation: userAnnotation,
annotations: annotations,
highlightedAnnotation: highlightedAnnotation,
showsUserLocation: showsUserLocation)
showsUserLocation: showsUserLocation,
isLiveLocationSharingEnabled: isLiveLocationSharingEnabled)
super.init(initialViewState: viewState)

View File

@@ -17,7 +17,7 @@
import SwiftUI
@available(iOS 14.0, *)
struct LocationSharingUserMarkerView: View {
struct LocationSharingMarkerView<Content: View>: View {
// MARK: - Properties
@@ -25,37 +25,21 @@ struct LocationSharingUserMarkerView: View {
@Environment(\.theme) private var theme: ThemeSwiftUI
@State private var frame: CGRect = .zero
private var usernameColorGenerator: UserNameColorGenerator {
let usernameColorGenerator = UserNameColorGenerator()
let theme = ThemeService.shared().theme
usernameColorGenerator.defaultColor = theme.textPrimaryColor
usernameColorGenerator.userNameColors = theme.userNameColors
return usernameColorGenerator
}
// MARK: Public
let isMarker: Bool
let avatarData: AvatarInputProtocol
let backgroundColor: Color
@ViewBuilder var markerImage: Content
var body: some View {
let fillColor: Color = Color(usernameColorGenerator.color(from:avatarData.matrixItemId))
ZStack {
Circle()
.fill(fillColor)
.frame(width: 40, height: 40)
if isMarker {
Rectangle()
.rotation(Angle(degrees: 45))
.fill(fillColor)
.frame(width: 7, height: 7)
.offset(x: 0, y: 19)
}
AvatarImage(avatarData: avatarData, size: .small)
Rectangle()
.rotation(Angle(degrees: 45))
.fill(backgroundColor)
.frame(width: 7, height: 7)
.offset(x: 0, y: 22)
markerImage
.frame(width: 42, height: 42)
}
.background(ViewFrameReader(frame: $frame))
}
}
@@ -68,8 +52,14 @@ struct LocationSharingUserMarkerView_Previews: PreviewProvider {
matrixItemId: "test",
displayName: "Alice")
VStack(alignment: .center, spacing: 15) {
LocationSharingUserMarkerView(isMarker: true, avatarData: avatarData)
LocationSharingUserMarkerView(isMarker: false, avatarData: avatarData)
LocationSharingMarkerView(backgroundColor: .green) {
AvatarImage(avatarData: avatarData, size: nil)
.shapedBorder(color: Color.green, borderWidth: 3, shape: Circle())
}
LocationSharingMarkerView(backgroundColor: .green) {
AvatarImage(avatarData: avatarData, size: nil)
.shapedBorder(color: Color.green, borderWidth: 3, shape: Circle())
}
}
}
}

View File

@@ -27,12 +27,12 @@ struct LocationSharingOptionButton<Content: View>: View {
let text: String
let action: () -> (Void)
@ViewBuilder var content: Content
@ViewBuilder var buttonIcon: Content
var body: some View {
Button(action: action) {
HStack(spacing: 18) {
content
buttonIcon
.frame(width: 40, height: 40)
Text(text)
.font(theme.fonts.body)
@@ -45,16 +45,22 @@ struct LocationSharingOptionButton<Content: View>: View {
@available(iOS 14.0, *)
struct LocationSharingOptionButton_Previews: PreviewProvider {
static var previews: some View {
VStack {
LocationSharingOptionButton(text: "Share my current location") {
VStack(alignment: .leading) {
LocationSharingOptionButton(text: VectorL10n.locationSharingStaticShareTitle) {
} content: {
LocationSharingUserMarkerView(isMarker: false, avatarData: AvatarInput(mxContentUri: "", matrixItemId: "test", displayName: "Nicolas"))
} buttonIcon: {
AvatarImage(avatarData: AvatarInput(mxContentUri: nil, matrixItemId: "Alice", displayName: "Alice"), size: nil)
.shapedBorder(color: Color.green, borderWidth: 3, shape: Circle())
}
LocationSharingOptionButton(text: "Share live location") {
LocationSharingOptionButton(text: VectorL10n.locationSharingLiveShareTitle) {
} content: {
LocationSharingOptionButtonIcon(fillColor: Color.purple, image: Asset.Images.liveLocationIcon.image)
} buttonIcon: {
Image(uiImage: Asset.Images.locationLiveIcon.image)
}
LocationSharingOptionButton(text: VectorL10n.locationSharingPinDropShareTitle) {
} buttonIcon: {
Image(uiImage: Asset.Images.locationPinIcon.image)
}
}
}

View File

@@ -1,48 +0,0 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
@available(iOS 14.0, *)
struct LocationSharingOptionButtonIcon: View {
// MARK: - Properties
// MARK: Private
@Environment(\.theme) private var theme: ThemeSwiftUI
let fillColor: Color
let image: UIImage
var body: some View {
ZStack {
Circle()
.fill(fillColor)
.frame(width: 40, height: 40)
Image(uiImage: image)
.renderingMode(.template)
.foregroundColor(Color.white)
}
}
}
@available(iOS 14.0, *)
struct LocationSharingOptionButtonIcon_Previews: PreviewProvider {
static var previews: some View {
LocationSharingOptionButtonIcon(fillColor: Color.green, image: Asset.Images.locationMarkerIcon.image)
}
}

View File

@@ -32,7 +32,7 @@ struct LocationSharingView: View {
var body: some View {
NavigationView {
VStack {
VStack(spacing: 0) {
ZStack(alignment: .bottom) {
LocationSharingMapView(tileServerMapURL: context.viewState.mapStyleURL,
annotations: context.viewState.annotations,
@@ -46,7 +46,7 @@ struct LocationSharingView: View {
}
if context.viewState.shareButtonVisible {
buttonsView
.cornerRadius(5)
.cornerRadius(10)
}
}
.toolbar {
@@ -90,25 +90,25 @@ struct LocationSharingView: View {
if !context.viewState.isPinDropSharing {
LocationSharingOptionButton(text: VectorL10n.locationSharingStaticShareTitle) {
context.send(viewAction: .share)
} content: {
LocationSharingUserMarkerView(isMarker: false, avatarData: context.viewState.userAvatarData)
} buttonIcon: {
AvatarImage(avatarData: context.viewState.userAvatarData, size: nil)
.shapedBorder(color: theme.displayUserColor(for: context.viewState.userAvatarData.matrixItemId), borderWidth: 3, shape: Circle())
}
.disabled(!context.viewState.shareButtonEnabled)
// Disable for now until live location sharing is done
if BuildSettings.liveLocationSharingEnabled {
// Hide for now until live location sharing is finished
if context.viewState.isLiveLocationSharingEnabled {
LocationSharingOptionButton(text: VectorL10n.locationSharingLiveShareTitle) {
// TODO: - Start live location sharing
} content: {
LocationSharingOptionButtonIcon(fillColor: Color.purple, image: Asset.Images.liveLocationIcon.image)
} buttonIcon: {
Image(uiImage: Asset.Images.locationLiveIcon.image)
}
.disabled(!context.viewState.shareButtonEnabled)
}
} else {
LocationSharingOptionButton(text: VectorL10n.locationSharingPinDropShareTitle) {
// TODO: - Pin drop sharing action
} content: {
LocationSharingOptionButtonIcon(fillColor:
theme.colors.primaryContent, image: Asset.Images.locationMarkerIcon.image)
} buttonIcon: {
Image(uiImage: Asset.Images.locationPinIcon.image)
}
.disabled(!context.viewState.shareButtonEnabled)
}

View File

@@ -21,6 +21,10 @@ import Mapbox
@available(iOS 14, *)
class UserLocationAnnotatonView: MGLUserLocationAnnotationView {
// MARK: Private
@Environment(\.theme) private var theme: ThemeSwiftUI
// MARK: - Setup
init(avatarData: AvatarInputProtocol) {
@@ -45,7 +49,10 @@ class UserLocationAnnotatonView: MGLUserLocationAnnotationView {
private func addUserMarkerView(with avatarData: AvatarInputProtocol) {
guard let avatarImageView = UIHostingController(rootView: LocationSharingUserMarkerView(isMarker: true, avatarData: avatarData)).view else {
guard let avatarImageView = UIHostingController(rootView: LocationSharingMarkerView(backgroundColor: theme.displayUserColor(for: avatarData.matrixItemId)) {
AvatarImage(avatarData: avatarData, size: nil)
.shapedBorder(color: theme.displayUserColor(for: avatarData.matrixItemId), borderWidth: 3, shape: Circle())
}).view else {
return
}

View File

@@ -35,7 +35,7 @@ struct TemplateRoomChatBubbleView: View {
.accessibility(identifier: "bubbleImage")
VStack(alignment: .leading){
Text(bubble.sender.displayName ?? "")
.foregroundColor(theme.displayNameColor(for: bubble.sender.id))
.foregroundColor(theme.displayUserColor(for: bubble.sender.id))
.font(theme.fonts.bodySB)
ForEach(bubble.items) { item in
TemplateRoomChatBubbleContentView(bubbleItem: item)