mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-29 07:42:40 +00:00
Merge branch 'develop' into ismail/5582_opening_threads
This commit is contained in:
commit
2ab7f9d896
29 changed files with 639 additions and 125 deletions
|
@ -1,9 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina5_5" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
|
@ -21,14 +22,14 @@
|
|||
<subviews>
|
||||
<imageView clearsContextBeforeDrawing="NO" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" insetsLayoutMarginsFromSafeArea="NO" image="launch_screen_logo" translatesAutoresizingMaskIntoConstraints="NO" id="IcW-17-3Qq">
|
||||
<rect key="frame" x="147" y="308" width="120" height="120"/>
|
||||
<color key="tintColor" red="0.051999999999999998" green="0.74299999999999999" blue="0.54300000000000004" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
<color key="tintColor" red="0.050980392156862744" green="0.74117647058823533" blue="0.54509803921568623" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="IcW-17-3Qq" secondAttribute="height" multiplier="1:1" id="nyP-l5-s1B"/>
|
||||
<constraint firstAttribute="width" constant="120" id="v1s-rq-3ay"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="IcW-17-3Qq" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="F70-sn-ghB"/>
|
||||
<constraint firstItem="IcW-17-3Qq" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="nJz-qK-nMP"/>
|
||||
|
@ -42,5 +43,8 @@
|
|||
</scenes>
|
||||
<resources>
|
||||
<image name="launch_screen_logo" width="240" height="240"/>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
|
|
|
@ -1945,6 +1945,8 @@ Tap the + to start adding people.";
|
|||
|
||||
"location_sharing_open_google_maps" = "Open in Google Maps";
|
||||
|
||||
"location_sharing_open_open_street_maps" = "Open in OpenStreetMap";
|
||||
|
||||
"location_sharing_settings_header" = "Location sharing";
|
||||
|
||||
"location_sharing_settings_toggle_title" = "Enable location sharing";
|
||||
|
|
|
@ -2267,6 +2267,10 @@ public class VectorL10n: NSObject {
|
|||
public static var locationSharingOpenGoogleMaps: String {
|
||||
return VectorL10n.tr("Vector", "location_sharing_open_google_maps")
|
||||
}
|
||||
/// Open in OpenStreetMap
|
||||
public static var locationSharingOpenOpenStreetMaps: String {
|
||||
return VectorL10n.tr("Vector", "location_sharing_open_open_street_maps")
|
||||
}
|
||||
/// %@ could not send your location. Please try again later.
|
||||
public static func locationSharingPostFailureSubtitle(_ p1: String) -> String {
|
||||
return VectorL10n.tr("Vector", "location_sharing_post_failure_subtitle", p1)
|
||||
|
|
|
@ -45,7 +45,7 @@ class DarkTheme: NSObject, Theme {
|
|||
var textTertiaryColor: UIColor = UIColor(rgb: 0x8E99A4)
|
||||
var textQuinaryColor: UIColor = UIColor(rgb: 0x394049)
|
||||
|
||||
var tintColor: UIColor = UIColor(displayP3Red: 0.05098039216, green: 0.7450980392, blue: 0.5450980392, alpha: 1.0)
|
||||
var tintColor: UIColor = UIColor(rgb: 0x0DBD8B)
|
||||
var tintBackgroundColor: UIColor = UIColor(rgb: 0x1F6954)
|
||||
var tabBarUnselectedItemTintColor: UIColor = UIColor(rgb: 0x8E99A4)
|
||||
var unreadRoomIndentColor: UIColor = UIColor(rgb: 0x2E3648)
|
||||
|
|
|
@ -45,7 +45,7 @@ class DefaultTheme: NSObject, Theme {
|
|||
var textTertiaryColor: UIColor = UIColor(rgb: 0x8D99A5)
|
||||
var textQuinaryColor: UIColor = UIColor(rgb: 0xE3E8F0)
|
||||
|
||||
var tintColor: UIColor = UIColor(displayP3Red: 0.05098039216, green: 0.7450980392, blue: 0.5450980392, alpha: 1.0)
|
||||
var tintColor: UIColor = UIColor(rgb: 0x0DBD8B)
|
||||
var tintBackgroundColor: UIColor = UIColor(rgb: 0xe9fff9)
|
||||
var tabBarUnselectedItemTintColor: UIColor = UIColor(rgb: 0xC1C6CD)
|
||||
var unreadRoomIndentColor: UIColor = UIColor(rgb: 0x2E3648)
|
||||
|
|
|
@ -171,6 +171,14 @@ import AnalyticsEvents
|
|||
// The following methods are exposed for compatibility with Objective-C as
|
||||
// the `capture` method and the generated events cannot be bridged from Swift.
|
||||
extension Analytics {
|
||||
/// Updates any user properties to help with creating cohorts.
|
||||
///
|
||||
/// Only non-nil properties will be updated when calling this method.
|
||||
func updateUserProperties(ftueUseCase: UserSessionProperties.UseCase? = nil) {
|
||||
let userProperties = AnalyticsEvent.UserProperties(ftueUseCaseSelection: ftueUseCase?.analyticsName, numSpaces: nil)
|
||||
client.updateUserProperties(userProperties)
|
||||
}
|
||||
|
||||
/// Track the presentation of a screen
|
||||
/// - Parameters:
|
||||
/// - screen: The screen that was shown.
|
||||
|
@ -186,20 +194,21 @@ extension Analytics {
|
|||
trackScreen(screen, duration: nil)
|
||||
}
|
||||
|
||||
/// Track an element that has been tapped
|
||||
/// Track an element that has been interacted with
|
||||
/// - Parameters:
|
||||
/// - tap: The element that was tapped
|
||||
/// - uiElement: The element that was interacted with
|
||||
/// - interactionType: The way in with the element was interacted with
|
||||
/// - index: The index of the element, if it's in a list of elements
|
||||
func trackTap(_ tap: AnalyticsUIElement, index: Int?) {
|
||||
let event = AnalyticsEvent.Click(index: index, name: tap.elementName)
|
||||
func trackInteraction(_ uiElement: AnalyticsUIElement, interactionType: AnalyticsEvent.Interaction.InteractionType, index: Int?) {
|
||||
let event = AnalyticsEvent.Interaction(index: index, interactionType: interactionType, name: uiElement.name)
|
||||
client.capture(event)
|
||||
}
|
||||
|
||||
/// Track an element that has been tapped without including an index
|
||||
/// - Parameters:
|
||||
/// - tap: The element that was tapped
|
||||
func trackTap(_ tap: AnalyticsUIElement) {
|
||||
trackTap(tap, index: nil)
|
||||
/// - uiElement: The element that was tapped
|
||||
func trackInteraction(_ uiElement: AnalyticsUIElement) {
|
||||
trackInteraction(uiElement, interactionType: .Touch, index: nil)
|
||||
}
|
||||
|
||||
/// Track an E2EE error that occurred
|
||||
|
|
|
@ -45,4 +45,12 @@ protocol AnalyticsClientProtocol {
|
|||
/// Capture the supplied analytics screen event.
|
||||
/// - Parameter event: The screen event to capture.
|
||||
func screen(_ event: AnalyticsScreenProtocol)
|
||||
|
||||
/// Updates any user properties to help with creating cohorts.
|
||||
/// - Parameter userProperties: The user properties to be updated.
|
||||
///
|
||||
/// Only non-nil properties will be updated when calling this method. There might
|
||||
/// be a delay when updating user properties as these are cached to be included
|
||||
/// as part of the next event that gets captured.
|
||||
func updateUserProperties(_ userProperties: AnalyticsEvent.UserProperties)
|
||||
}
|
||||
|
|
|
@ -18,6 +18,10 @@ import Foundation
|
|||
import AnalyticsEvents
|
||||
|
||||
@objc enum AnalyticsScreen: Int {
|
||||
case welcome
|
||||
case login
|
||||
case register
|
||||
case forgotPassword
|
||||
case sidebar
|
||||
case home
|
||||
case favourites
|
||||
|
@ -51,6 +55,14 @@ import AnalyticsEvents
|
|||
/// The screen name reported to the AnalyticsEvent.
|
||||
var screenName: AnalyticsEvent.Screen.ScreenName {
|
||||
switch self {
|
||||
case .welcome:
|
||||
return .Welcome
|
||||
case .login:
|
||||
return .Login
|
||||
case .register:
|
||||
return .Register
|
||||
case .forgotPassword:
|
||||
return .ForgotPassword
|
||||
case .sidebar:
|
||||
return .MobileSidebar
|
||||
case .home:
|
||||
|
|
|
@ -16,17 +16,17 @@
|
|||
|
||||
import AnalyticsEvents
|
||||
|
||||
/// A tappable UI element that can be track in Analytics.
|
||||
/// A tappable UI element that can be tracked in Analytics.
|
||||
@objc enum AnalyticsUIElement: Int {
|
||||
case sendMessageButton
|
||||
case removeMe
|
||||
|
||||
/// The element name reported to the AnalyticsEvent.
|
||||
var elementName: AnalyticsEvent.Click.Name {
|
||||
var name: AnalyticsEvent.Interaction.Name {
|
||||
switch self {
|
||||
// Note: This is a test element that doesn't need to be captured.
|
||||
// It will likely be removed when the AnalyticsEvent.Click is updated.
|
||||
case .sendMessageButton:
|
||||
return .SendMessageButton
|
||||
// It can be removed when some elements are added that relate to mobile.
|
||||
case .removeMe:
|
||||
return .WebRoomSettingsLeaveButton
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
//
|
||||
// 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 AnalyticsEvents
|
||||
|
||||
extension UserSessionProperties.UseCase {
|
||||
var analyticsName: AnalyticsEvent.UserProperties.FtueUseCaseSelection {
|
||||
switch self {
|
||||
case .personalMessaging:
|
||||
return .PersonalMessaging
|
||||
case .workMessaging:
|
||||
return .WorkMessaging
|
||||
case .communityMessaging:
|
||||
return .CommunityMessaging
|
||||
case .skipped:
|
||||
return .Skip
|
||||
}
|
||||
}
|
||||
}
|
|
@ -22,6 +22,9 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol {
|
|||
/// The PHGPostHog object used to report events.
|
||||
private var postHog: PHGPostHog?
|
||||
|
||||
/// Any user properties to be included with the next captured event.
|
||||
private(set) var pendingUserProperties: AnalyticsEvent.UserProperties?
|
||||
|
||||
var isRunning: Bool { postHog?.enabled ?? false }
|
||||
|
||||
func start() {
|
||||
|
@ -36,11 +39,18 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol {
|
|||
}
|
||||
|
||||
func identify(id: String) {
|
||||
postHog?.identify(id)
|
||||
if let userProperties = pendingUserProperties {
|
||||
// As user properties overwrite old ones, compactMap the dictionary to avoid resetting any missing properties
|
||||
postHog?.identify(id, properties: userProperties.properties.compactMapValues { $0 })
|
||||
pendingUserProperties = nil
|
||||
} else {
|
||||
postHog?.identify(id)
|
||||
}
|
||||
}
|
||||
|
||||
func reset() {
|
||||
postHog?.reset()
|
||||
pendingUserProperties = nil
|
||||
}
|
||||
|
||||
func stop() {
|
||||
|
@ -55,11 +65,38 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol {
|
|||
}
|
||||
|
||||
func capture(_ event: AnalyticsEventProtocol) {
|
||||
postHog?.capture(event.eventName, properties: event.properties)
|
||||
postHog?.capture(event.eventName, properties: attachUserProperties(to: event.properties))
|
||||
}
|
||||
|
||||
func screen(_ event: AnalyticsScreenProtocol) {
|
||||
postHog?.screen(event.screenName.rawValue, properties: event.properties)
|
||||
postHog?.screen(event.screenName.rawValue, properties: attachUserProperties(to: event.properties))
|
||||
}
|
||||
|
||||
func updateUserProperties(_ userProperties: AnalyticsEvent.UserProperties) {
|
||||
guard let pendingUserProperties = pendingUserProperties else {
|
||||
pendingUserProperties = userProperties
|
||||
return
|
||||
}
|
||||
|
||||
// Merge the updated user properties with the existing ones
|
||||
self.pendingUserProperties = AnalyticsEvent.UserProperties(ftueUseCaseSelection: userProperties.ftueUseCaseSelection ?? pendingUserProperties.ftueUseCaseSelection,
|
||||
numSpaces: userProperties.numSpaces ?? pendingUserProperties.numSpaces)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// Given a dictionary containing properties from an event, this method will return those properties
|
||||
/// with any pending user properties included under the `$set` key.
|
||||
/// - Parameter properties: A dictionary of properties from an event.
|
||||
/// - Returns: The `properties` dictionary with any user properties included.
|
||||
private func attachUserProperties(to properties: [String: Any]) -> [String: Any] {
|
||||
guard isRunning, let userProperties = pendingUserProperties else { return properties }
|
||||
|
||||
var properties = properties
|
||||
|
||||
// As user properties overwrite old ones via $set, compactMap the dictionary to avoid resetting any missing properties
|
||||
properties["$set"] = userProperties.properties.compactMapValues { $0 }
|
||||
pendingUserProperties = nil
|
||||
return properties
|
||||
}
|
||||
}
|
||||
|
|
|
@ -102,7 +102,7 @@ public class ElementView: UIView {
|
|||
path.layer.bounds = CGRect(x: 0, y: 0, width: 55.2, height: 55.2)
|
||||
path.layer.masksToBounds = false
|
||||
path.shapeLayer.fillRule = CAShapeLayerFillRule.evenOdd
|
||||
path.shapeLayer.fillColor = UIColor(displayP3Red: 0.052, green: 0.743, blue: 0.543, alpha: 1).cgColor
|
||||
path.shapeLayer.fillColor = UIColor(rgb: 0x0DBD8B).cgColor
|
||||
path.shapeLayer.lineDashPattern = []
|
||||
path.shapeLayer.lineDashPhase = 0
|
||||
path.shapeLayer.lineWidth = 0
|
||||
|
@ -120,7 +120,7 @@ public class ElementView: UIView {
|
|||
path_1.layer.bounds = CGRect(x: 0, y: 0, width: 55.2, height: 55.2)
|
||||
path_1.layer.masksToBounds = false
|
||||
path_1.shapeLayer.fillRule = CAShapeLayerFillRule.evenOdd
|
||||
path_1.shapeLayer.fillColor = UIColor(displayP3Red: 0.052, green: 0.743, blue: 0.543, alpha: 1).cgColor
|
||||
path_1.shapeLayer.fillColor = UIColor(rgb: 0x0DBD8B).cgColor
|
||||
path_1.shapeLayer.lineDashPattern = []
|
||||
path_1.shapeLayer.lineDashPhase = 0
|
||||
path_1.shapeLayer.lineWidth = 0
|
||||
|
@ -138,7 +138,7 @@ public class ElementView: UIView {
|
|||
path_2.layer.bounds = CGRect(x: 0, y: 0, width: 55.2, height: 55.2)
|
||||
path_2.layer.masksToBounds = false
|
||||
path_2.shapeLayer.fillRule = CAShapeLayerFillRule.evenOdd
|
||||
path_2.shapeLayer.fillColor = UIColor(displayP3Red: 0.052, green: 0.743, blue: 0.543, alpha: 1).cgColor
|
||||
path_2.shapeLayer.fillColor = UIColor(rgb: 0x0DBD8B).cgColor
|
||||
path_2.shapeLayer.lineDashPattern = []
|
||||
path_2.shapeLayer.lineDashPhase = 0
|
||||
path_2.shapeLayer.lineWidth = 0
|
||||
|
@ -156,7 +156,7 @@ public class ElementView: UIView {
|
|||
path_3.layer.bounds = CGRect(x: 0, y: 0, width: 55.2, height: 55.2)
|
||||
path_3.layer.masksToBounds = false
|
||||
path_3.shapeLayer.fillRule = CAShapeLayerFillRule.evenOdd
|
||||
path_3.shapeLayer.fillColor = UIColor(displayP3Red: 0.052, green: 0.743, blue: 0.543, alpha: 1).cgColor
|
||||
path_3.shapeLayer.fillColor = UIColor(rgb: 0x0DBD8B).cgColor
|
||||
path_3.shapeLayer.lineDashPattern = []
|
||||
path_3.shapeLayer.lineDashPhase = 0
|
||||
path_3.shapeLayer.lineWidth = 0
|
||||
|
|
|
@ -36,7 +36,17 @@ public class MarkdownToHTMLRenderer: NSObject {
|
|||
extension MarkdownToHTMLRenderer: MarkdownToHTMLRendererProtocol {
|
||||
|
||||
public func renderToHTML(markdown: String) -> String? {
|
||||
return try? Down(markdownString: markdown).toHTML(options)
|
||||
do {
|
||||
let ast = try DownASTRenderer.stringToAST(markdown, options: options)
|
||||
defer {
|
||||
cmark_node_free(ast)
|
||||
}
|
||||
ast.repairLinks()
|
||||
return try DownHTMLRenderer.astToHTML(ast, options: options)
|
||||
} catch {
|
||||
MXLog.error("[MarkdownToHTMLRenderer] renderToHTML failed with string: \(markdown)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -50,3 +60,116 @@ public class MarkdownToHTMLRendererHardBreaks: MarkdownToHTMLRenderer {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - AST-handling private extensions
|
||||
private extension CMarkNode {
|
||||
/// Formatting symbol associated with given note type
|
||||
/// Note: this is only defined for node types that are handled in repairLinks
|
||||
var formattingSymbol: String {
|
||||
switch self.type {
|
||||
case CMARK_NODE_EMPH:
|
||||
return "_"
|
||||
case CMARK_NODE_STRONG:
|
||||
return "__"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
/// Repairs links that were broken down by markdown formatting.
|
||||
/// Should be used on the first node of libcmark's AST
|
||||
/// (e.g. the object returned by DownASTRenderer.stringToAST).
|
||||
func repairLinks() {
|
||||
let iterator = cmark_iter_new(self)
|
||||
var text = ""
|
||||
var isInParagraph = false
|
||||
var previousNode: CMarkNode?
|
||||
var orphanNodes: [CMarkNode] = []
|
||||
var shouldUnlinkFormattingMode = false
|
||||
var event: cmark_event_type?
|
||||
while event != CMARK_EVENT_DONE {
|
||||
event = cmark_iter_next(iterator)
|
||||
|
||||
guard let node = cmark_iter_get_node(iterator) else { return }
|
||||
|
||||
if node.type == CMARK_NODE_PARAGRAPH {
|
||||
if event == CMARK_EVENT_ENTER {
|
||||
isInParagraph = true
|
||||
} else {
|
||||
isInParagraph = false
|
||||
text = ""
|
||||
}
|
||||
}
|
||||
|
||||
if isInParagraph {
|
||||
switch node.type {
|
||||
case CMARK_NODE_SOFTBREAK,
|
||||
CMARK_NODE_LINEBREAK:
|
||||
text = ""
|
||||
case CMARK_NODE_TEXT:
|
||||
if let literal = node.literal {
|
||||
text += literal
|
||||
// Reset text if it ends up with a whitespace.
|
||||
if text.last?.isWhitespace == true {
|
||||
text = ""
|
||||
}
|
||||
// Only the last part could be a link conflicting with next node.
|
||||
text = String(text.split(separator: " ").last ?? "")
|
||||
}
|
||||
case CMARK_NODE_EMPH where previousNode?.type == CMARK_NODE_TEXT,
|
||||
CMARK_NODE_STRONG where previousNode?.type == CMARK_NODE_TEXT:
|
||||
if event == CMARK_EVENT_ENTER {
|
||||
if !text.containedUrls.isEmpty,
|
||||
let childLiteral = node.pointee.first_child.literal {
|
||||
// If current text is a link, the formatted text is reverted back to a
|
||||
// plain text as a part of the link.
|
||||
let symbol = node.formattingSymbol
|
||||
let nonFormattedText = "\(symbol)\(childLiteral)\(symbol)"
|
||||
let replacementTextNode = cmark_node_new(CMARK_NODE_TEXT)
|
||||
cmark_node_set_literal(replacementTextNode, nonFormattedText)
|
||||
cmark_node_insert_after(previousNode, replacementTextNode)
|
||||
// Set child literal to empty string so we dont read it.
|
||||
// This avoids having to re-create the main
|
||||
// iterator in the middle of the process.
|
||||
cmark_node_set_literal(node.pointee.first_child, "")
|
||||
let newIterator = cmark_iter_new(node)
|
||||
_ = cmark_iter_next(newIterator)
|
||||
cmark_node_unlink(node)
|
||||
orphanNodes.append(node)
|
||||
let nextNode = cmark_iter_get_node(newIterator)
|
||||
cmark_node_insert_after(previousNode, nextNode)
|
||||
shouldUnlinkFormattingMode = true
|
||||
}
|
||||
} else {
|
||||
if shouldUnlinkFormattingMode {
|
||||
cmark_node_unlink(node)
|
||||
orphanNodes.append(node)
|
||||
shouldUnlinkFormattingMode = false
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
previousNode = node
|
||||
}
|
||||
|
||||
// Free all nodes removed from the AST.
|
||||
// This is done as a last step to avoid messing
|
||||
// up with the main itertor.
|
||||
for orphanNode in orphanNodes {
|
||||
cmark_node_free(orphanNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
/// Returns array of URLs detected inside the String.
|
||||
var containedUrls: [NSTextCheckingResult] {
|
||||
guard let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else {
|
||||
return []
|
||||
}
|
||||
|
||||
return detector.matches(in: self, options: [], range: NSRange(location: 0, length: self.utf16.count))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -147,10 +147,10 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
|||
@available(iOS 14.0, *)
|
||||
/// Show the use case screen for new users.
|
||||
private func showUseCaseSelectionScreen() {
|
||||
let coordinator = OnboardingUseCaseCoordinator()
|
||||
let coordinator = OnboardingUseCaseSelectionCoordinator()
|
||||
coordinator.completion = { [weak self, weak coordinator] result in
|
||||
guard let self = self, let coordinator = coordinator else { return }
|
||||
self.useCaseCoordinator(coordinator, didCompleteWith: result)
|
||||
self.useCaseSelectionCoordinator(coordinator, didCompleteWith: result)
|
||||
}
|
||||
|
||||
coordinator.start()
|
||||
|
@ -166,7 +166,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
|||
}
|
||||
|
||||
/// Displays the next view in the flow after the use case screen.
|
||||
private func useCaseCoordinator(_ coordinator: OnboardingUseCaseCoordinator, didCompleteWith result: OnboardingUseCaseViewModelResult) {
|
||||
private func useCaseSelectionCoordinator(_ coordinator: OnboardingUseCaseSelectionCoordinator, didCompleteWith result: OnboardingUseCaseViewModelResult) {
|
||||
useCaseResult = result
|
||||
showAuthenticationScreen()
|
||||
}
|
||||
|
@ -222,12 +222,15 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
|||
completion?()
|
||||
isShowingAuthentication = false
|
||||
|
||||
// Handle the chosen use case if appropriate
|
||||
// Handle the chosen use case where applicable
|
||||
if authenticationType == MXKAuthenticationTypeRegister,
|
||||
let useCaseResult = useCaseResult,
|
||||
let useCase = useCaseResult?.userSessionPropertyValue,
|
||||
let userSession = UserSessionsService.shared.mainUserSession {
|
||||
// Store the value in the user's session
|
||||
userSession.userProperties.useCase = useCaseResult.userSessionPropertyValue
|
||||
userSession.userProperties.useCase = useCase
|
||||
|
||||
// Update the analytics user properties with the use case
|
||||
Analytics.shared.updateUserProperties(ftueUseCase: useCase)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat
|
|||
@IBOutlet private var descriptionContainerView: UIView!
|
||||
@IBOutlet private var descriptionLabel: UILabel!
|
||||
@IBOutlet private var descriptionIcon: UIImageView!
|
||||
@IBOutlet private var attributionLabel: UILabel!
|
||||
|
||||
private var mapView: MGLMapView!
|
||||
private var annotationView: LocationMarkerView?
|
||||
|
@ -101,6 +102,7 @@ class RoomTimelineLocationView: UIView, NibLoadable, Themable, MGLMapViewDelegat
|
|||
descriptionLabel.textColor = theme.colors.primaryContent
|
||||
descriptionLabel.font = theme.fonts.footnote
|
||||
descriptionIcon.tintColor = theme.colors.accent
|
||||
attributionLabel.textColor = theme.colors.accent
|
||||
layer.borderColor = theme.colors.quinaryContent.cgColor
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<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="18093"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
|
@ -15,48 +15,72 @@
|
|||
<rect key="frame" x="0.0" y="0.0" width="395" height="250"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="oVd-gS-Rmb">
|
||||
<rect key="frame" x="0.0" y="210" width="395" height="40"/>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="0D1-Km-vTu">
|
||||
<rect key="frame" x="0.0" y="137.5" width="395" height="112.5"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="XHz-4S-fh4">
|
||||
<rect key="frame" x="12" y="8" width="371" height="24"/>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="HzR-Av-TiG">
|
||||
<rect key="frame" x="0.0" y="0.0" width="395" height="54.5"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="location_marker_icon" translatesAutoresizingMaskIntoConstraints="NO" id="GP2-dA-giJ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="24" height="24"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="24" id="7nK-Kb-7Iq"/>
|
||||
<constraint firstAttribute="height" constant="24" id="nBW-gN-0uW"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="c68-l7-McA">
|
||||
<rect key="frame" x="32" y="2" width="339" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="©MapTiler ©OpenStreetMap contributors" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="jWW-0w-1YM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="387" height="46.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="jWW-0w-1YM" firstAttribute="top" secondItem="HzR-Av-TiG" secondAttribute="top" id="1Hd-f0-Is6"/>
|
||||
<constraint firstItem="jWW-0w-1YM" firstAttribute="leading" secondItem="HzR-Av-TiG" secondAttribute="leading" id="R9R-Za-1Li"/>
|
||||
<constraint firstAttribute="trailing" secondItem="jWW-0w-1YM" secondAttribute="trailing" constant="8" id="Up7-yC-9tX"/>
|
||||
<constraint firstAttribute="bottom" secondItem="jWW-0w-1YM" secondAttribute="bottom" constant="8" id="t8L-m8-q4c"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="oVd-gS-Rmb">
|
||||
<rect key="frame" x="0.0" y="62.5" width="395" height="50"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="XHz-4S-fh4">
|
||||
<rect key="frame" x="12" y="8" width="371" height="34"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="location_marker_icon" translatesAutoresizingMaskIntoConstraints="NO" id="GP2-dA-giJ">
|
||||
<rect key="frame" x="0.0" y="5" width="24" height="24"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="24" id="7nK-Kb-7Iq"/>
|
||||
<constraint firstAttribute="height" constant="24" id="nBW-gN-0uW"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="c68-l7-McA">
|
||||
<rect key="frame" x="32" y="7" width="339" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="XHz-4S-fh4" secondAttribute="trailing" constant="12" id="FI1-B7-bPV"/>
|
||||
<constraint firstItem="XHz-4S-fh4" firstAttribute="top" secondItem="oVd-gS-Rmb" secondAttribute="top" constant="8" id="UJq-Yz-ikR"/>
|
||||
<constraint firstAttribute="bottom" secondItem="XHz-4S-fh4" secondAttribute="bottom" constant="8" id="cvr-Gb-uLe"/>
|
||||
<constraint firstItem="XHz-4S-fh4" firstAttribute="leading" secondItem="oVd-gS-Rmb" secondAttribute="leading" constant="12" id="wSE-NS-2h4"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="XHz-4S-fh4" secondAttribute="trailing" constant="12" id="FI1-B7-bPV"/>
|
||||
<constraint firstItem="XHz-4S-fh4" firstAttribute="top" secondItem="oVd-gS-Rmb" secondAttribute="top" constant="8" id="UJq-Yz-ikR"/>
|
||||
<constraint firstAttribute="bottom" secondItem="XHz-4S-fh4" secondAttribute="bottom" constant="8" id="cvr-Gb-uLe"/>
|
||||
<constraint firstItem="XHz-4S-fh4" firstAttribute="leading" secondItem="oVd-gS-Rmb" secondAttribute="leading" constant="12" id="wSE-NS-2h4"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="oVd-gS-Rmb" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="6Vw-QI-iN2"/>
|
||||
<constraint firstItem="oVd-gS-Rmb" firstAttribute="bottom" secondItem="vUN-kp-3ea" secondAttribute="bottom" id="FVf-yb-Gxc"/>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="oVd-gS-Rmb" secondAttribute="trailing" id="O3u-fm-TxC"/>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="0D1-Km-vTu" secondAttribute="trailing" id="QHD-xv-nfX"/>
|
||||
<constraint firstAttribute="bottom" secondItem="0D1-Km-vTu" secondAttribute="bottom" id="ea5-xx-V3s"/>
|
||||
<constraint firstItem="0D1-Km-vTu" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="yJw-DU-ien"/>
|
||||
</constraints>
|
||||
<nil key="simulatedTopBarMetrics"/>
|
||||
<nil key="simulatedBottomBarMetrics"/>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<connections>
|
||||
<outlet property="attributionLabel" destination="jWW-0w-1YM" id="MtF-th-LJr"/>
|
||||
<outlet property="descriptionContainerView" destination="oVd-gS-Rmb" id="Npu-jp-oYo"/>
|
||||
<outlet property="descriptionIcon" destination="GP2-dA-giJ" id="7YL-UU-ClT"/>
|
||||
<outlet property="descriptionLabel" destination="c68-l7-McA" id="HiH-8Q-yTp"/>
|
||||
|
|
|
@ -20,7 +20,7 @@ import Foundation
|
|||
@available(iOS 14.0, *)
|
||||
enum MockAppScreens {
|
||||
static let appScreens: [MockScreenState.Type] = [
|
||||
MockOnboardingUseCaseScreenState.self,
|
||||
MockOnboardingUseCaseSelectionScreenState.self,
|
||||
MockOnboardingSplashScreenScreenState.self,
|
||||
MockLocationSharingScreenState.self,
|
||||
MockAnalyticsPromptScreenState.self,
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
final class OnboardingUseCaseCoordinator: Coordinator, Presentable {
|
||||
final class OnboardingUseCaseSelectionCoordinator: Coordinator, Presentable {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
|
@ -36,7 +36,7 @@ final class OnboardingUseCaseCoordinator: Coordinator, Presentable {
|
|||
@available(iOS 14.0, *)
|
||||
init() {
|
||||
let viewModel = OnboardingUseCaseViewModel()
|
||||
let view = OnboardingUseCase(viewModel: viewModel.context)
|
||||
let view = OnboardingUseCaseSelectionScreen(viewModel: viewModel.context)
|
||||
onboardingUseCaseViewModel = viewModel
|
||||
|
||||
let hostingController = VectorHostingController(rootView: view)
|
||||
|
@ -47,9 +47,9 @@ final class OnboardingUseCaseCoordinator: Coordinator, Presentable {
|
|||
|
||||
// MARK: - Public
|
||||
func start() {
|
||||
MXLog.debug("[OnboardingUseCaseCoordinator] did start.")
|
||||
MXLog.debug("[OnboardingUseCaseSelectionCoordinator] did start.")
|
||||
onboardingUseCaseViewModel.completion = { [weak self] result in
|
||||
MXLog.debug("[OnboardingUseCaseCoordinator] OnboardingUseCaseViewModel did complete with result: \(result).")
|
||||
MXLog.debug("[OnboardingUseCaseSelectionCoordinator] OnboardingUseCaseViewModel did complete with result: \(result).")
|
||||
guard let self = self else { return }
|
||||
self.completion?(result)
|
||||
}
|
|
@ -20,7 +20,7 @@ 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 MockOnboardingUseCaseScreenState: MockScreenState, CaseIterable {
|
||||
enum MockOnboardingUseCaseSelectionScreenState: MockScreenState, CaseIterable {
|
||||
// A case for each state you want to represent
|
||||
// with specific, minimal associated data that will allow you
|
||||
// mock that screen.
|
||||
|
@ -28,11 +28,11 @@ enum MockOnboardingUseCaseScreenState: MockScreenState, CaseIterable {
|
|||
|
||||
/// The associated screen
|
||||
var screenType: Any.Type {
|
||||
OnboardingUseCase.self
|
||||
OnboardingUseCaseSelectionScreen.self
|
||||
}
|
||||
|
||||
/// A list of screen state definitions
|
||||
static var allCases: [MockOnboardingUseCaseScreenState] {
|
||||
static var allCases: [MockOnboardingUseCaseSelectionScreenState] {
|
||||
// Each of the presence statuses
|
||||
[.default]
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ enum MockOnboardingUseCaseScreenState: MockScreenState, CaseIterable {
|
|||
|
||||
return (
|
||||
[self, viewModel],
|
||||
AnyView(OnboardingUseCase(viewModel: viewModel.context)
|
||||
AnyView(OnboardingUseCaseSelectionScreen(viewModel: viewModel.context)
|
||||
.addDependency(MockAvatarService.example))
|
||||
)
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ import SwiftUI
|
|||
|
||||
@available(iOS 14.0, *)
|
||||
/// The screen shown to a new user to select their use case for the app.
|
||||
struct OnboardingUseCase: View {
|
||||
struct OnboardingUseCaseSelectionScreen: View {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
|
@ -119,7 +119,7 @@ struct OnboardingUseCase: View {
|
|||
|
||||
@available(iOS 14.0, *)
|
||||
struct OnboardingUseCase_Previews: PreviewProvider {
|
||||
static let stateRenderer = MockOnboardingUseCaseScreenState.stateRenderer
|
||||
static let stateRenderer = MockOnboardingUseCaseSelectionScreenState.stateRenderer
|
||||
static var previews: some View {
|
||||
NavigationView {
|
||||
stateRenderer.screenGroup()
|
|
@ -97,7 +97,8 @@ final class LocationSharingCoordinator: Coordinator, Presentable {
|
|||
static func shareLocationActivityController(_ location: CLLocationCoordinate2D) -> UIActivityViewController {
|
||||
return UIActivityViewController(activityItems: [ShareToMapsAppActivity.urlForMapsAppType(.apple, location: location)],
|
||||
applicationActivities: [ShareToMapsAppActivity(type: .apple, location: location),
|
||||
ShareToMapsAppActivity(type: .google, location: location)])
|
||||
ShareToMapsAppActivity(type: .google, location: location),
|
||||
ShareToMapsAppActivity(type: .osm, location: location)])
|
||||
}
|
||||
|
||||
// MARK: - Presentable
|
||||
|
|
|
@ -24,6 +24,7 @@ class ShareToMapsAppActivity: UIActivity {
|
|||
enum MapsAppType {
|
||||
case apple
|
||||
case google
|
||||
case osm
|
||||
}
|
||||
|
||||
private let type: MapsAppType
|
||||
|
@ -44,6 +45,8 @@ class ShareToMapsAppActivity: UIActivity {
|
|||
return URL(string: "https://maps.apple.com?ll=\(location.latitude),\(location.longitude)&q=Pin")!
|
||||
case .google:
|
||||
return URL(string: "https://www.google.com/maps/search/?api=1&query=\(location.latitude),\(location.longitude)")!
|
||||
case .osm:
|
||||
return URL(string: "https://www.openstreetmap.org/?mlat=\(location.latitude)&mlon=\(location.longitude)")!
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -53,6 +56,8 @@ class ShareToMapsAppActivity: UIActivity {
|
|||
return VectorL10n.locationSharingOpenAppleMaps
|
||||
case .google:
|
||||
return VectorL10n.locationSharingOpenGoogleMaps
|
||||
case .osm:
|
||||
return VectorL10n.locationSharingOpenOpenStreetMaps
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -32,62 +32,70 @@ struct LocationSharingView: View {
|
|||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
LocationSharingMapView(tileServerMapURL: context.viewState.mapStyleURL,
|
||||
avatarData: context.viewState.avatarData,
|
||||
location: context.viewState.location,
|
||||
errorSubject: context.viewState.errorSubject,
|
||||
userLocation: $context.userLocation)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button(VectorL10n.cancel, action: {
|
||||
context.send(viewAction: .cancel)
|
||||
})
|
||||
}
|
||||
ToolbarItem(placement: .principal) {
|
||||
Text(VectorL10n.locationSharingTitle)
|
||||
.font(.headline)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
if context.viewState.location != nil {
|
||||
Button {
|
||||
context.send(viewAction: .share)
|
||||
} label: {
|
||||
Image(uiImage: Asset.Images.locationShareIcon.image)
|
||||
.accessibilityIdentifier("LocationSharingView.shareButton")
|
||||
}
|
||||
.disabled(!context.viewState.shareButtonEnabled)
|
||||
} else {
|
||||
Button(VectorL10n.locationSharingShareAction, action: {
|
||||
context.send(viewAction: .share)
|
||||
})
|
||||
.disabled(!context.viewState.shareButtonEnabled)
|
||||
ZStack(alignment: .bottom) {
|
||||
LocationSharingMapView(tileServerMapURL: context.viewState.mapStyleURL,
|
||||
avatarData: context.viewState.avatarData,
|
||||
location: context.viewState.location,
|
||||
errorSubject: context.viewState.errorSubject,
|
||||
userLocation: $context.userLocation)
|
||||
.ignoresSafeArea()
|
||||
|
||||
HStack {
|
||||
Link("© MapTiler", destination: URL(string: "https://www.maptiler.com/copyright/")!)
|
||||
Link("© OpenStreetMap contributors", destination: URL(string: "https://www.openstreetmap.org/copyright")!)
|
||||
}
|
||||
.font(theme.fonts.caption1)
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button(VectorL10n.cancel, action: {
|
||||
context.send(viewAction: .cancel)
|
||||
})
|
||||
}
|
||||
ToolbarItem(placement: .principal) {
|
||||
Text(VectorL10n.locationSharingTitle)
|
||||
.font(.headline)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
if context.viewState.location != nil {
|
||||
Button {
|
||||
context.send(viewAction: .share)
|
||||
} label: {
|
||||
Image(uiImage: Asset.Images.locationShareIcon.image)
|
||||
.accessibilityIdentifier("LocationSharingView.shareButton")
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.introspectNavigationController { navigationController in
|
||||
ThemeService.shared().theme.applyStyle(onNavigationBar: navigationController.navigationBar)
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.alert(item: $context.alertInfo) { info in
|
||||
if let secondaryButton = info.secondaryButton {
|
||||
return Alert(title: Text(info.title),
|
||||
message: subtitleTextForAlertInfo(info),
|
||||
primaryButton: .default(Text(info.primaryButton.title)) {
|
||||
info.primaryButton.action?()
|
||||
},
|
||||
secondaryButton: .default(Text(secondaryButton.title)) {
|
||||
secondaryButton.action?()
|
||||
})
|
||||
.disabled(!context.viewState.shareButtonEnabled)
|
||||
} else {
|
||||
return Alert(title: Text(info.title),
|
||||
message: subtitleTextForAlertInfo(info),
|
||||
dismissButton: .default(Text(info.primaryButton.title)) {
|
||||
info.primaryButton.action?()
|
||||
})
|
||||
Button(VectorL10n.locationSharingShareAction, action: {
|
||||
context.send(viewAction: .share)
|
||||
})
|
||||
.disabled(!context.viewState.shareButtonEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.introspectNavigationController { navigationController in
|
||||
ThemeService.shared().theme.applyStyle(onNavigationBar: navigationController.navigationBar)
|
||||
}
|
||||
.alert(item: $context.alertInfo) { info in
|
||||
if let secondaryButton = info.secondaryButton {
|
||||
return Alert(title: Text(info.title),
|
||||
message: subtitleTextForAlertInfo(info),
|
||||
primaryButton: .default(Text(info.primaryButton.title)) {
|
||||
info.primaryButton.action?()
|
||||
},
|
||||
secondaryButton: .default(Text(secondaryButton.title)) {
|
||||
secondaryButton.action?()
|
||||
})
|
||||
} else {
|
||||
return Alert(title: Text(info.title),
|
||||
message: subtitleTextForAlertInfo(info),
|
||||
dismissButton: .default(Text(info.primaryButton.title)) {
|
||||
info.primaryButton.action?()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
.accentColor(theme.colors.accent)
|
||||
.activityIndicator(show: context.viewState.showLoadingIndicator)
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
import XCTest
|
||||
@testable import Riot
|
||||
import AnalyticsEvents
|
||||
|
||||
class AnalyticsTests: XCTestCase {
|
||||
func testAnalyticsPromptNewUser() {
|
||||
|
@ -70,4 +71,68 @@ class AnalyticsTests: XCTestCase {
|
|||
// Then no prompt should be shown.
|
||||
XCTAssertFalse(showPrompt, "A prompt should not be shown any more.")
|
||||
}
|
||||
|
||||
func testAddingUserProperties() {
|
||||
// Given a client with no user properties set
|
||||
let client = PostHogAnalyticsClient()
|
||||
XCTAssertNil(client.pendingUserProperties, "No user properties should have been set yet.")
|
||||
|
||||
// When updating the user properties
|
||||
client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: .PersonalMessaging, numSpaces: 5))
|
||||
|
||||
// Then the properties should be cached
|
||||
XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
|
||||
XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection should match.")
|
||||
XCTAssertEqual(client.pendingUserProperties?.numSpaces, 5, "The number of spaces should match.")
|
||||
}
|
||||
|
||||
func testMergingUserProperties() {
|
||||
// Given a client with a cached use case user properties
|
||||
let client = PostHogAnalyticsClient()
|
||||
client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: .PersonalMessaging, numSpaces: nil))
|
||||
|
||||
XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
|
||||
XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection should match.")
|
||||
XCTAssertNil(client.pendingUserProperties?.numSpaces, "The number of spaces should not be set.")
|
||||
|
||||
// When updating the number of spaced
|
||||
client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: nil, numSpaces: 5))
|
||||
|
||||
// Then the new properties should be updated and the existing properties should remain unchanged
|
||||
XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
|
||||
XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection shouldn't have changed.")
|
||||
XCTAssertEqual(client.pendingUserProperties?.numSpaces, 5, "The number of spaces should have been updated.")
|
||||
}
|
||||
|
||||
func testSendingUserProperties() {
|
||||
// Given a client with user properties set
|
||||
let client = PostHogAnalyticsClient()
|
||||
client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: .PersonalMessaging, numSpaces: nil))
|
||||
client.start()
|
||||
|
||||
XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
|
||||
XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection should match.")
|
||||
|
||||
// When sending an event (tests run under Debug configuration so this is sent to the development instance)
|
||||
client.screen(AnalyticsEvent.Screen(durationMs: nil, screenName: .Home))
|
||||
|
||||
// Then the properties should be cleared
|
||||
XCTAssertNil(client.pendingUserProperties, "The user properties should be cleared.")
|
||||
}
|
||||
|
||||
func testSendingUserPropertiesWithIdentify() {
|
||||
// Given a client with user properties set
|
||||
let client = PostHogAnalyticsClient()
|
||||
client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: .PersonalMessaging, numSpaces: nil))
|
||||
client.start()
|
||||
|
||||
XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
|
||||
XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection should match.")
|
||||
|
||||
// When calling identify (tests run under Debug configuration so this is sent to the development instance)
|
||||
client.identify(id: UUID().uuidString)
|
||||
|
||||
// Then the properties should be cleared
|
||||
XCTAssertNil(client.pendingUserProperties, "The user properties should be cleared.")
|
||||
}
|
||||
}
|
||||
|
|
170
RiotTests/MarkdownToHTMLRendererTests.swift
Normal file
170
RiotTests/MarkdownToHTMLRendererTests.swift
Normal file
|
@ -0,0 +1,170 @@
|
|||
//
|
||||
// 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 XCTest
|
||||
|
||||
@testable import Riot
|
||||
|
||||
final class MarkdownToHTMLRendererTests: XCTestCase {
|
||||
// MARK: - Tests
|
||||
/// Test autolinks HTML render.
|
||||
func testRenderAutolinks() {
|
||||
let input = [
|
||||
"Test1:",
|
||||
"<#_foonetic_xkcd:matrix.org>",
|
||||
"<http://google.com/_thing_>",
|
||||
"<https://matrix.org/_matrix/client/foo/123_>",
|
||||
"<#_foonetic_xkcd:matrix.org>",
|
||||
"",
|
||||
"Test1A:",
|
||||
"<#_foonetic_xkcd:matrix.org>",
|
||||
"<http://google.com/_thing_>",
|
||||
"<https://matrix.org/_matrix/client/foo/123_>",
|
||||
"<#_foonetic_xkcd:matrix.org>",
|
||||
"",
|
||||
"Test2:",
|
||||
"<http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg>",
|
||||
"<http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg>",
|
||||
"",
|
||||
"Test3:",
|
||||
"<https://riot.im/app/#/room/#_foonetic_xkcd:matrix.org>",
|
||||
"<https://riot.im/app/#/room/#_foonetic_xkcd:matrix.org>",
|
||||
].joined(separator: "\n")
|
||||
let expectedOutput = [
|
||||
"<p>Test1:\n<#_foonetic_xkcd:matrix.org>\n<a href=\"http://google.com/_thing_\">http://google.com/_thing_</a>\n<a href=\"https://matrix.org/_matrix/client/foo/123_\">https://matrix.org/_matrix/client/foo/123_</a>\n<#_foonetic_xkcd:matrix.org></p>",
|
||||
"<p>Test1A:\n<#_foonetic_xkcd:matrix.org>\n<a href=\"http://google.com/_thing_\">http://google.com/_thing_</a>\n<a href=\"https://matrix.org/_matrix/client/foo/123_\">https://matrix.org/_matrix/client/foo/123_</a>\n<#_foonetic_xkcd:matrix.org></p>",
|
||||
"<p>Test2:\n<a href=\"http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg\">http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg</a>\n<a href=\"http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg\">http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg</a></p>",
|
||||
"<p>Test3:\n<a href=\"https://riot.im/app/#/room/#_foonetic_xkcd:matrix.org\">https://riot.im/app/#/room/#_foonetic_xkcd:matrix.org</a>\n<a href=\"https://riot.im/app/#/room/#_foonetic_xkcd:matrix.org\">https://riot.im/app/#/room/#_foonetic_xkcd:matrix.org</a></p>",
|
||||
"",
|
||||
].joined(separator: "\n")
|
||||
testRenderHTML(input: input, expectedOutput: expectedOutput)
|
||||
}
|
||||
|
||||
/// Test links with markdown formatting conflict.
|
||||
func testRenderRepairedLinks() {
|
||||
let input = [
|
||||
"Test1:",
|
||||
"#_foonetic_xkcd:matrix.org",
|
||||
"http://google.com/_thing_",
|
||||
"https://matrix.org/_matrix/client/foo/123_",
|
||||
"#_foonetic_xkcd:matrix.org",
|
||||
"",
|
||||
"Test1A:",
|
||||
"#_foonetic_xkcd:matrix.org",
|
||||
"http://google.com/_thing_",
|
||||
"https://matrix.org/_matrix/client/foo/123_",
|
||||
"#_foonetic_xkcd:matrix.org",
|
||||
"",
|
||||
"Test2:",
|
||||
"http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg",
|
||||
"http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg",
|
||||
"",
|
||||
"Test3:",
|
||||
"https://riot.im/app/#/room/#_foonetic_xkcd:matrix.org",
|
||||
"https://riot.im/app/#/room/#_foonetic_xkcd:matrix.org",
|
||||
].joined(separator: "\n")
|
||||
let expectedOutput = [
|
||||
"<p>Test1:\n#_foonetic_xkcd:matrix.org\nhttp://google.com/_thing_\nhttps://matrix.org/_matrix/client/foo/123_\n#_foonetic_xkcd:matrix.org</p>",
|
||||
"<p>Test1A:\n#_foonetic_xkcd:matrix.org\nhttp://google.com/_thing_\nhttps://matrix.org/_matrix/client/foo/123_\n#_foonetic_xkcd:matrix.org</p>",
|
||||
"<p>Test2:\nhttp://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg\nhttp://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg</p>",
|
||||
"<p>Test3:\nhttps://riot.im/app/#/room/#_foonetic_xkcd:matrix.org\nhttps://riot.im/app/#/room/#_foonetic_xkcd:matrix.org</p>",
|
||||
"",
|
||||
].joined(separator: "\n")
|
||||
testRenderHTML(input: input, expectedOutput: expectedOutput)
|
||||
}
|
||||
|
||||
/// Test links with markdown strong formatting conflict.
|
||||
func testRenderRepairedLinksWithStrongFormatting() {
|
||||
let input = "https://github.com/matrix-org/synapse/blob/develop/synapse/module_api/__init__.py"
|
||||
+ " "
|
||||
+ "https://github.com/matrix-org/synapse/blob/develop/synapse/module_api/__init__.py"
|
||||
let expectedOutput = "<p>https://github.com/matrix-org/synapse/blob/develop/synapse/module_api/__init__.py"
|
||||
+ " "
|
||||
+ "https://github.com/matrix-org/synapse/blob/develop/synapse/module_api/__init__.py</p>"
|
||||
+ "\n"
|
||||
testRenderHTML(input: input, expectedOutput: expectedOutput)
|
||||
}
|
||||
|
||||
/// Test links with markdown formatting conflict and actual markdown in between.
|
||||
func testRenderRepairedLinksWithMarkdownInBetween() {
|
||||
let input = "__Some bold text__ "
|
||||
+ "https://github.com/matrix-org/synapse/blob/develop/synapse/module_api/__init__.py"
|
||||
+ " _some emphased text_ "
|
||||
+ "http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg"
|
||||
let expectedOutput = "<p><strong>Some bold text</strong> "
|
||||
+ "https://github.com/matrix-org/synapse/blob/develop/synapse/module_api/__init__.py"
|
||||
+ " <em>some emphased text</em> "
|
||||
+ "http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg</p>"
|
||||
+ "\n"
|
||||
testRenderHTML(input: input, expectedOutput: expectedOutput)
|
||||
}
|
||||
|
||||
/// Test links inside codeblocks.
|
||||
func testRenderLinksInCodeblock() {
|
||||
let input = "```"
|
||||
+ [
|
||||
"Test1:",
|
||||
"#_foonetic_xkcd:matrix.org",
|
||||
"http://google.com/_thing_",
|
||||
"https://matrix.org/_matrix/client/foo/123_",
|
||||
"#_foonetic_xkcd:matrix.org",
|
||||
"",
|
||||
"Test1A:",
|
||||
"#_foonetic_xkcd:matrix.org",
|
||||
"http://google.com/_thing_",
|
||||
"https://matrix.org/_matrix/client/foo/123_",
|
||||
"#_foonetic_xkcd:matrix.org",
|
||||
"",
|
||||
"Test2:",
|
||||
"http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg",
|
||||
"http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg",
|
||||
"",
|
||||
"Test3:",
|
||||
"https://riot.im/app/#/room/#_foonetic_xkcd:matrix.org",
|
||||
"https://riot.im/app/#/room/#_foonetic_xkcd:matrix.org",
|
||||
].joined(separator: "\n")
|
||||
+ "```"
|
||||
let expectedOutput = [
|
||||
"<pre><code class=\"language-Test1:\">#_foonetic_xkcd:matrix.org",
|
||||
"http://google.com/_thing_",
|
||||
"https://matrix.org/_matrix/client/foo/123_",
|
||||
"#_foonetic_xkcd:matrix.org",
|
||||
"",
|
||||
"Test1A:",
|
||||
"#_foonetic_xkcd:matrix.org",
|
||||
"http://google.com/_thing_",
|
||||
"https://matrix.org/_matrix/client/foo/123_",
|
||||
"#_foonetic_xkcd:matrix.org",
|
||||
"",
|
||||
"Test2:",
|
||||
"http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg",
|
||||
"http://domain.xyz/foo/bar-_stuff-like-this_-in-it.jpg",
|
||||
"",
|
||||
"Test3:",
|
||||
"https://riot.im/app/#/room/#_foonetic_xkcd:matrix.org",
|
||||
"https://riot.im/app/#/room/#_foonetic_xkcd:matrix.org```",
|
||||
"</code></pre>",
|
||||
"",
|
||||
].joined(separator: "\n")
|
||||
testRenderHTML(input: input, expectedOutput: expectedOutput)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
private func testRenderHTML(input: String, expectedOutput: String) {
|
||||
let output = MarkdownToHTMLRenderer().renderToHTML(markdown: input)
|
||||
XCTAssertEqual(output, expectedOutput)
|
||||
}
|
||||
}
|
1
changelog.d/5355.bugfix
Normal file
1
changelog.d/5355.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Markdown/HTML: Fix HTTP links containing Markdown formatting
|
1
changelog.d/5545.bugfix
Normal file
1
changelog.d/5545.bugfix
Normal file
|
@ -0,0 +1 @@
|
|||
Update the tintColor in ThemeV1 to sRGB to match the Compound and ThemeV2.
|
1
changelog.d/5590.change
Normal file
1
changelog.d/5590.change
Normal file
|
@ -0,0 +1 @@
|
|||
Add support for UserProperties to analytics and capture FTUE use case selection.
|
1
changelog.d/5609.change
Normal file
1
changelog.d/5609.change
Normal file
|
@ -0,0 +1 @@
|
|||
Add attribution to location sharing maps.
|
Loading…
Reference in a new issue