Merge branch 'develop' into ismail/5582_opening_threads

This commit is contained in:
ismailgulek 2022-02-18 14:27:51 +03:00
commit 2ab7f9d896
No known key found for this signature in database
GPG key ID: E96336D42D9470A9
29 changed files with 639 additions and 125 deletions

View file

@ -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>

View file

@ -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";

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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)
}

View file

@ -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:

View file

@ -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
}
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}

View file

@ -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

View file

@ -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))
}
}

View file

@ -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)
}
}
}

View file

@ -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
}

View file

@ -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"/>

View file

@ -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,

View file

@ -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)
}

View file

@ -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))
)
}

View file

@ -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()

View file

@ -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

View file

@ -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
}
}

View file

@ -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)

View file

@ -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.")
}
}

View 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&lt;#_foonetic_xkcd:matrix.org&gt;\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&lt;#_foonetic_xkcd:matrix.org&gt;</p>",
"<p>Test1A:\n&lt;#_foonetic_xkcd:matrix.org&gt;\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&lt;#_foonetic_xkcd:matrix.org&gt;</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
View file

@ -0,0 +1 @@
Markdown/HTML: Fix HTTP links containing Markdown formatting

1
changelog.d/5545.bugfix Normal file
View file

@ -0,0 +1 @@
Update the tintColor in ThemeV1 to sRGB to match the Compound and ThemeV2.

1
changelog.d/5590.change Normal file
View file

@ -0,0 +1 @@
Add support for UserProperties to analytics and capture FTUE use case selection.

1
changelog.d/5609.change Normal file
View file

@ -0,0 +1 @@
Add attribution to location sharing maps.