Merge branch 'develop' into alfogrillo/badges_spaces

This commit is contained in:
Alfonso Grillo 2022-11-25 10:53:48 +01:00
commit 9e2e2d03a3
44 changed files with 298 additions and 69 deletions

View file

@ -4,7 +4,6 @@ on:
# Triggers the workflow on any pull request # Triggers the workflow on any pull request
pull_request: pull_request:
types: [ labeled, synchronized, opened, reopened ]
# Allows you to run this workflow manually from the Actions tab # Allows you to run this workflow manually from the Actions tab
workflow_dispatch: workflow_dispatch:

View file

@ -409,7 +409,7 @@ final class BuildSettings: NSObject {
// MARK: - Voice Broadcast // MARK: - Voice Broadcast
static let voiceBroadcastChunkLength: Int = 120 static let voiceBroadcastChunkLength: Int = 120
static let voiceBroadcastMaxLength: UInt64 = 144000 static let voiceBroadcastMaxLength: UInt = 14400 // 240min.
// MARK: - MXKAppSettings // MARK: - MXKAppSettings
static let enableBotCreation: Bool = false static let enableBotCreation: Bool = false

View file

@ -23,7 +23,7 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift", "location" : "https://github.com/matrix-org/matrix-wysiwyg-composer-swift",
"state" : { "state" : {
"revision" : "2469f27b7e1e51aaa135e09f9005eb10fda686e6" "revision" : "1fbffd0321eb47abcd664ad19c6c943b60abf399"
} }
}, },
{ {

View file

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "voice_broadcast_time_left.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 1H6V2.33333H10V1ZM7.33333 9.66667H8.66667V5.66667H7.33333V9.66667ZM12.6867 5.26L13.6333 4.31333C13.3467 3.97333 13.0333 3.65333 12.6933 3.37333L11.7467 4.32C10.7133 3.49333 9.41333 3 8 3C4.68667 3 2 5.68667 2 9C2 12.3133 4.68 15 8 15C11.32 15 14 12.3133 14 9C14 7.58667 13.5067 6.28667 12.6867 5.26ZM8 13.6667C5.42 13.6667 3.33333 11.58 3.33333 9C3.33333 6.42 5.42 4.33333 8 4.33333C10.58 4.33333 12.6667 6.42 12.6667 9C12.6667 11.58 10.58 13.6667 8 13.6667Z" fill="#737D8C"/>
</svg>

After

Width:  |  Height:  |  Size: 593 B

View file

@ -1968,6 +1968,12 @@ Tap the + to start adding people.";
"call_transfer_error_title" = "Error"; "call_transfer_error_title" = "Error";
"call_transfer_error_message" = "Call transfer failed"; "call_transfer_error_message" = "Call transfer failed";
// MARK: - Launch loading
"launch_loading_server_syncing" = "Syncing with the server";
"launch_loading_server_syncing_nth_attempt" = "Syncing with the server\n(%@ attempt)";
"launch_loading_processing_response" = "Processing data\n%@ %%";
// MARK: - Home // MARK: - Home
"home_empty_view_title" = "Welcome to %@,\n%@"; "home_empty_view_title" = "Welcome to %@,\n%@";
@ -2196,6 +2202,7 @@ Tap the + to start adding people.";
"voice_broadcast_playback_loading_error" = "Unable to play this voice broadcast."; "voice_broadcast_playback_loading_error" = "Unable to play this voice broadcast.";
"voice_broadcast_live" = "Live"; "voice_broadcast_live" = "Live";
"voice_broadcast_tile" = "Voice broadcast"; "voice_broadcast_tile" = "Voice broadcast";
"voice_broadcast_time_left" = "%@ left";
// Mark: - Version check // Mark: - Version check

View file

@ -347,6 +347,7 @@ internal class Asset: NSObject {
internal static let voiceBroadcastStop = ImageAsset(name: "voice_broadcast_stop") internal static let voiceBroadcastStop = ImageAsset(name: "voice_broadcast_stop")
internal static let voiceBroadcastTileLive = ImageAsset(name: "voice_broadcast_tile_live") internal static let voiceBroadcastTileLive = ImageAsset(name: "voice_broadcast_tile_live")
internal static let voiceBroadcastTileMic = ImageAsset(name: "voice_broadcast_tile_mic") internal static let voiceBroadcastTileMic = ImageAsset(name: "voice_broadcast_tile_mic")
internal static let voiceBroadcastTimeLeft = ImageAsset(name: "voice_broadcast_time_left")
internal static let launchScreenLogo = ImageAsset(name: "launch_screen_logo") internal static let launchScreenLogo = ImageAsset(name: "launch_screen_logo")
} }
@objcMembers @objcMembers

View file

@ -3179,6 +3179,18 @@ public class VectorL10n: NSObject {
public static var later: String { public static var later: String {
return VectorL10n.tr("Vector", "later") return VectorL10n.tr("Vector", "later")
} }
/// Processing data\n%@ %%
public static func launchLoadingProcessingResponse(_ p1: String) -> String {
return VectorL10n.tr("Vector", "launch_loading_processing_response", p1)
}
/// Syncing with the server
public static var launchLoadingServerSyncing: String {
return VectorL10n.tr("Vector", "launch_loading_server_syncing")
}
/// Syncing with the server\n(%@ attempt)
public static func launchLoadingServerSyncingNthAttempt(_ p1: String) -> String {
return VectorL10n.tr("Vector", "launch_loading_server_syncing_nth_attempt", p1)
}
/// Leave /// Leave
public static var leave: String { public static var leave: String {
return VectorL10n.tr("Vector", "leave") return VectorL10n.tr("Vector", "leave")
@ -9143,6 +9155,10 @@ public class VectorL10n: NSObject {
public static var voiceBroadcastTile: String { public static var voiceBroadcastTile: String {
return VectorL10n.tr("Vector", "voice_broadcast_tile") return VectorL10n.tr("Vector", "voice_broadcast_tile")
} }
/// %@ left
public static func voiceBroadcastTimeLeft(_ p1: String) -> String {
return VectorL10n.tr("Vector", "voice_broadcast_time_left", p1)
}
/// Can't start a new voice broadcast /// Can't start a new voice broadcast
public static var voiceBroadcastUnauthorizedTitle: String { public static var voiceBroadcastUnauthorizedTitle: String {
return VectorL10n.tr("Vector", "voice_broadcast_unauthorized_title") return VectorL10n.tr("Vector", "voice_broadcast_unauthorized_title")

View file

@ -42,6 +42,10 @@ struct SentryMonitoringClient {
options.enableNetworkTracking = false options.enableNetworkTracking = false
options.beforeSend = { event in options.beforeSend = { event in
// Use the actual error message as issue fingerprint
if let message = event.message?.formatted {
event.fingerprint = [message]
}
MXLog.debug("[SentryMonitoringClient] Issue detected: \(event)") MXLog.debug("[SentryMonitoringClient] Issue detected: \(event)")
return event return event
} }

View file

@ -2394,7 +2394,17 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
{ {
MXLogDebug(@"[AppDelegate] showLaunchAnimation"); MXLogDebug(@"[AppDelegate] showLaunchAnimation");
LaunchLoadingView *launchLoadingView = [LaunchLoadingView instantiate]; LaunchLoadingView *launchLoadingView;
if (MXSDKOptions.sharedInstance.enableSyncProgress)
{
MXSession *mainSession = self.mxSessions.firstObject;
launchLoadingView = [LaunchLoadingView instantiateWithSyncProgress:mainSession.syncProgress];
}
else
{
launchLoadingView = [LaunchLoadingView instantiateWithSyncProgress:nil];
}
launchLoadingView.frame = window.bounds; launchLoadingView.frame = window.bounds;
[launchLoadingView updateWithTheme:ThemeService.shared.theme]; [launchLoadingView updateWithTheme:ThemeService.shared.theme];
launchLoadingView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; launchLoadingView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;

View file

@ -613,7 +613,8 @@ final class AuthenticationCoordinator: NSObject, AuthenticationCoordinatorProtoc
/// Replace the contents of the navigation router with a loading animation. /// Replace the contents of the navigation router with a loading animation.
private func showLoadingAnimation() { private func showLoadingAnimation() {
let loadingViewController = LaunchLoadingViewController() let syncProgress: MXSessionSyncProgress? = MXSDKOptions.sharedInstance().enableSyncProgress ? session?.syncProgress : nil
let loadingViewController = LaunchLoadingViewController(syncProgress: syncProgress)
loadingViewController.modalPresentationStyle = .fullScreen loadingViewController.modalPresentationStyle = .fullScreen
// Replace the navigation stack with the loading animation // Replace the navigation stack with the loading animation

View file

@ -106,7 +106,8 @@ final class LegacyAuthenticationCoordinator: NSObject, AuthenticationCoordinator
// MARK: - Private // MARK: - Private
private func showLoadingAnimation() { private func showLoadingAnimation() {
let loadingViewController = LaunchLoadingViewController() let syncProgress: MXSessionSyncProgress? = MXSDKOptions.sharedInstance().enableSyncProgress ? session?.syncProgress : nil
let loadingViewController = LaunchLoadingViewController(syncProgress: syncProgress)
loadingViewController.modalPresentationStyle = .fullScreen loadingViewController.modalPresentationStyle = .fullScreen
// Replace the navigation stack with the loading animation // Replace the navigation stack with the loading animation

View file

@ -30,12 +30,20 @@ final class LaunchLoadingView: UIView, NibLoadable, Themable {
// MARK: - Properties // MARK: - Properties
@IBOutlet private weak var animationView: ElementView! @IBOutlet private weak var animationView: ElementView!
@IBOutlet private weak var statusLabel: UILabel!
private var animationTimeline: Timeline_1! private var animationTimeline: Timeline_1!
private let numberFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .ordinal
return formatter
}()
// MARK: - Setup // MARK: - Setup
static func instantiate() -> LaunchLoadingView { static func instantiate(syncProgress: MXSessionSyncProgress?) -> LaunchLoadingView {
let view = LaunchLoadingView.loadFromNib() let view = LaunchLoadingView.loadFromNib()
syncProgress?.delegate = view
return view return view
} }
@ -45,6 +53,8 @@ final class LaunchLoadingView: UIView, NibLoadable, Themable {
let animationTimeline = Timeline_1(view: self.animationView, duration: LaunchAnimation.duration, repeatCount: LaunchAnimation.repeatCount) let animationTimeline = Timeline_1(view: self.animationView, duration: LaunchAnimation.duration, repeatCount: LaunchAnimation.repeatCount)
animationTimeline.play() animationTimeline.play()
self.animationTimeline = animationTimeline self.animationTimeline = animationTimeline
self.statusLabel.isHidden = !MXSDKOptions.sharedInstance().enableSyncProgress
} }
// MARK: - Public // MARK: - Public
@ -54,3 +64,31 @@ final class LaunchLoadingView: UIView, NibLoadable, Themable {
self.animationView.backgroundColor = theme.backgroundColor self.animationView.backgroundColor = theme.backgroundColor
} }
} }
extension LaunchLoadingView: MXSessionSyncProgressDelegate {
func sessionDidUpdateSyncState(_ state: MXSessionSyncState) {
guard MXSDKOptions.sharedInstance().enableSyncProgress else {
return
}
// Sync may be doing a lot of heavy work on the main thread and the status text
// does not update reliably enough without explicitly refreshing
CATransaction.begin()
statusLabel.text = statusText(for: state)
CATransaction.commit()
}
private func statusText(for state: MXSessionSyncState) -> String {
switch state {
case .serverSyncing(let attempts):
if attempts > 1, let nth = numberFormatter.string(from: NSNumber(value: attempts)) {
return VectorL10n.launchLoadingServerSyncingNthAttempt(nth)
} else {
return VectorL10n.launchLoadingServerSyncing
}
case .processingResponse(let progress):
let percent = Int(floor(progress * 100))
return VectorL10n.launchLoadingProcessingResponse("\(percent)")
}
}
}

View file

@ -1,32 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16097" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_0" orientation="portrait" appearance="light"/> <device id="retina4_0" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16097"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<objects> <objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/> <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="iN0-l3-epB" customClass="LaunchLoadingView" customModule="Riot" customModuleProvider="target"> <view contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="iN0-l3-epB" customClass="LaunchLoadingView" customModule="Element" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/> <rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<view contentMode="scaleAspectFit" insetsLayoutMarginsFromSafeArea="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3KG-IR-FPV" customClass="ElementView" customModule="Riot" customModuleProvider="target"> <view contentMode="scaleAspectFit" insetsLayoutMarginsFromSafeArea="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3KG-IR-FPV" customClass="ElementView" customModule="Element" customModuleProvider="target">
<rect key="frame" x="95" y="219" width="130" height="130"/> <rect key="frame" x="95" y="219" width="130" height="130"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/> <color key="backgroundColor" systemColor="systemBackgroundColor"/>
</view> </view>
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="center" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wzS-bN-Pht">
<rect key="frame" x="20" y="528" width="280" height="0.0"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" systemColor="systemGrayColor"/>
<nil key="highlightedColor"/>
</label>
</subviews> </subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/> <color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints> <constraints>
<constraint firstAttribute="trailing" secondItem="wzS-bN-Pht" secondAttribute="trailing" constant="20" id="Naf-Cc-qLq"/>
<constraint firstAttribute="bottom" secondItem="wzS-bN-Pht" secondAttribute="bottom" constant="40" id="cnE-Pn-Wb2"/>
<constraint firstItem="3KG-IR-FPV" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="ig4-YX-FoT"/> <constraint firstItem="3KG-IR-FPV" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="ig4-YX-FoT"/>
<constraint firstItem="3KG-IR-FPV" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="r9K-7c-fjh"/> <constraint firstItem="3KG-IR-FPV" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="r9K-7c-fjh"/>
<constraint firstItem="wzS-bN-Pht" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="20" id="uZP-JW-dVR"/>
</constraints> </constraints>
<connections> <connections>
<outlet property="animationView" destination="3KG-IR-FPV" id="Are-fn-laY"/> <outlet property="animationView" destination="3KG-IR-FPV" id="Are-fn-laY"/>
<outlet property="statusLabel" destination="wzS-bN-Pht" id="Mj2-rn-i5x"/>
</connections> </connections>
<point key="canvasLocation" x="136.875" y="132.5"/> <point key="canvasLocation" x="136.875" y="132.5"/>
</view> </view>
</objects> </objects>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<systemColor name="systemGrayColor">
<color red="0.55686274509803924" green="0.55686274509803924" blue="0.57647058823529407" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document> </document>

View file

@ -21,10 +21,10 @@ class LaunchLoadingViewController: UIViewController, Reusable {
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
init() { init(syncProgress: MXSessionSyncProgress?) {
super.init(nibName: "LaunchLoadingViewController", bundle: nil) super.init(nibName: "LaunchLoadingViewController", bundle: nil)
let launchLoadingView = LaunchLoadingView.instantiate() let launchLoadingView = LaunchLoadingView.instantiate(syncProgress: syncProgress)
launchLoadingView.update(theme: ThemeService.shared().theme) launchLoadingView.update(theme: ThemeService.shared().theme)
view.vc_addSubViewMatchingParent(launchLoadingView) view.vc_addSubViewMatchingParent(launchLoadingView)

View file

@ -36,7 +36,6 @@
<outlet property="userSuggestionContainerView" destination="oni-F4-X1U" id="0js-Ji-8Mm"/> <outlet property="userSuggestionContainerView" destination="oni-F4-X1U" id="0js-Ji-8Mm"/>
<outlet property="view" destination="iN0-l3-epB" id="ieV-u7-rXU"/> <outlet property="view" destination="iN0-l3-epB" id="ieV-u7-rXU"/>
<outletCollection property="toolbarContainerConstraints" destination="T1Y-r9-bYV" id="wax-9P-KGn"/> <outletCollection property="toolbarContainerConstraints" destination="T1Y-r9-bYV" id="wax-9P-KGn"/>
<outletCollection property="toolbarContainerConstraints" destination="ave-fu-X1D" id="gBl-7F-srT"/>
<outletCollection property="toolbarContainerConstraints" destination="pRw-S0-6WL" id="q4S-0g-sqQ"/> <outletCollection property="toolbarContainerConstraints" destination="pRw-S0-6WL" id="q4S-0g-sqQ"/>
<outletCollection property="toolbarContainerConstraints" destination="QO8-nF-xys" id="aQe-20-4Pq"/> <outletCollection property="toolbarContainerConstraints" destination="QO8-nF-xys" id="aQe-20-4Pq"/>
<outletCollection property="toolbarContainerConstraints" destination="acJ-g8-R7x" id="uEo-Ez-seV"/> <outletCollection property="toolbarContainerConstraints" destination="acJ-g8-R7x" id="uEo-Ez-seV"/>

View file

@ -65,6 +65,12 @@ class SizableBaseRoomCell: BaseRoomCell, SizableBaseRoomCellType {
return self.height(for: roomBubbleCellData, fitting: maxWidth) return self.height(for: roomBubbleCellData, fitting: maxWidth)
} }
override func prepareForReuse() {
cleanContentVC()
super.prepareForReuse()
}
// MARK - SizableBaseRoomCellType // MARK - SizableBaseRoomCellType
@ -173,10 +179,21 @@ class SizableBaseRoomCell: BaseRoomCell, SizableBaseRoomCellType {
} }
return height return height
} }
private func cleanContentVC() {
contentVC?.removeFromParent()
contentVC?.view.removeFromSuperview()
contentVC?.didMove(toParent: nil)
contentVC = nil
}
// MARK: - Public
func addContentViewController(_ controller: UIViewController, on contentView: UIView) { func addContentViewController(_ controller: UIViewController, on contentView: UIView) {
controller.view.invalidateIntrinsicContentSize() controller.view.invalidateIntrinsicContentSize()
cleanContentVC()
let parent = vc_parentViewController let parent = vc_parentViewController
parent?.addChild(controller) parent?.addChild(controller)
@ -185,13 +202,4 @@ class SizableBaseRoomCell: BaseRoomCell, SizableBaseRoomCellType {
contentVC = controller contentVC = controller
} }
override func prepareForReuse() {
contentVC?.removeFromParent()
contentVC?.view.removeFromSuperview()
contentVC?.didMove(toParent: nil)
contentVC = nil
super.prepareForReuse()
}
} }

View file

@ -43,9 +43,12 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
private var voiceMessageBottomConstraint: NSLayoutConstraint? private var voiceMessageBottomConstraint: NSLayoutConstraint?
private var hostingViewController: VectorHostingController! private var hostingViewController: VectorHostingController!
private var wysiwygViewModel = WysiwygComposerViewModel(textColor: ThemeService.shared().theme.colors.primaryContent) private var wysiwygViewModel = WysiwygComposerViewModel(textColor: ThemeService.shared().theme.colors.primaryContent)
private var viewModel: ComposerViewModelProtocol = ComposerViewModel( private var viewModel: ComposerViewModelProtocol!
initialViewState: ComposerViewState(textFormattingEnabled: RiotSettings.shared.enableWysiwygTextFormatting,
bindings: ComposerBindings(focused: false))) private var isLandscapePhone: Bool {
let device = UIDevice.current
return device.isPhone && device.orientation.isLandscape
}
// MARK: Public // MARK: Public
@ -99,6 +102,9 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
override func awakeFromNib() { override func awakeFromNib() {
super.awakeFromNib() super.awakeFromNib()
viewModel = ComposerViewModel(
initialViewState: ComposerViewState(textFormattingEnabled: RiotSettings.shared.enableWysiwygTextFormatting,
isLandscapePhone: isLandscapePhone, bindings: ComposerBindings(focused: false)))
viewModel.callback = { [weak self] result in viewModel.callback = { [weak self] result in
self?.handleViewModelResult(result) self?.handleViewModelResult(result)
@ -153,6 +159,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
}, },
wysiwygViewModel.$maximised wysiwygViewModel.$maximised
.dropFirst()
.removeDuplicates() .removeDuplicates()
.sink { [weak self] value in .sink { [weak self] value in
guard let self = self else { return } guard let self = self else { return }
@ -229,6 +236,7 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
} }
@objc private func deviceDidRotate(_ notification: Notification) { @objc private func deviceDidRotate(_ notification: Notification) {
viewModel.isLandscapePhone = isLandscapePhone
DispatchQueue.main.async { DispatchQueue.main.async {
self.updateTextViewHeight() self.updateTextViewHeight()
} }
@ -286,9 +294,14 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
private func updateTextViewHeight() { private func updateTextViewHeight() {
let height = UIScreen.main.bounds.height let height = UIScreen.main.bounds.height
let barOffset: CGFloat = 68 let barOffset: CGFloat = 68
let toolbarHeight: CGFloat = 82 let toolbarHeight: CGFloat = 96
let finalHeight = height - keyboardHeight - toolbarHeight - barOffset let finalHeight = height - keyboardHeight - toolbarHeight - barOffset
wysiwygViewModel.maxExpandedHeight = finalHeight wysiwygViewModel.maxExpandedHeight = finalHeight
if finalHeight < 200 {
wysiwygViewModel.maxCompressedHeight = finalHeight > wysiwygViewModel.minHeight ? finalHeight : wysiwygViewModel.minHeight
} else {
wysiwygViewModel.maxCompressedHeight = 200
}
} }
// MARK: - HtmlRoomInputToolbarViewProtocol // MARK: - HtmlRoomInputToolbarViewProtocol
@ -337,9 +350,6 @@ class WysiwygInputToolbarView: MXKRoomInputToolbarView, NibLoadable, HtmlRoomInp
set { set {
self.viewModel.textFormattingEnabled = newValue self.viewModel.textFormattingEnabled = newValue
self.wysiwygViewModel.plainTextMode = !newValue self.wysiwygViewModel.plainTextMode = !newValue
if !newValue {
self.wysiwygViewModel.maximised = false
}
} }
} }

View file

@ -61,7 +61,8 @@ class VoiceMessageAudioPlayer: NSObject {
} }
var currentTime: TimeInterval { var currentTime: TimeInterval {
return abs(CMTimeGetSeconds(audioPlayer?.currentTime() ?? .zero)) let currentTime = abs(CMTimeGetSeconds(audioPlayer?.currentTime() ?? .zero))
return currentTime.isFinite ? currentTime : .zero
} }
var playerItems: [AVPlayerItem] { var playerItems: [AVPlayerItem] {

View file

@ -64,28 +64,22 @@ private extension InfoSheetCoordinator {
// The bottom sheet should be presented with the content intrinsic height as for design requirement // The bottom sheet should be presented with the content intrinsic height as for design requirement
// We can do it easily just on iOS 16+ // We can do it easily just on iOS 16+
func setupPresentation(of viewController: VectorHostingController) { func setupPresentation(of viewController: VectorHostingController) {
let cornerRadius: CGFloat = 24 let detents: [VectorHostingBottomSheetPreferences.Detent]
guard if
#available(iOS 16, *), #available(iOS 16, *),
let parentSize = parameters.parentSize, let parentSize = parameters.parentSize {
let presentationController = viewController.sheetPresentationController
else { let intrisincSize = viewController.view.systemLayoutSizeFitting(.init(width: parentSize.width, height: UIView.layoutFittingCompressedSize.height),
viewController.bottomSheetPreferences = .init(cornerRadius: cornerRadius) withHorizontalFittingPriority: .defaultHigh,
return verticalFittingPriority: .defaultLow)
detents = [.custom(height: intrisincSize.height), .large]
} else {
detents = [.medium, .large]
} }
let intrisincSize = viewController.view.systemLayoutSizeFitting(.init(width: parentSize.width, height: 0), viewController.bottomSheetPreferences = .init(detents: detents, cornerRadius: 24)
withHorizontalFittingPriority: .defaultHigh, viewController.bottomSheetPreferences?.setup(viewController: viewController)
verticalFittingPriority: .defaultLow)
presentationController.preferredCornerRadius = cornerRadius
presentationController.prefersGrabberVisible = true
presentationController.detents = [
.custom { context in
min(context.maximumDetentValue, intrisincSize.height)
},
.large()
]
} }
} }

View file

@ -32,9 +32,9 @@ enum MockComposerScreenState: MockScreenState, CaseIterable {
let bindings = ComposerBindings(focused: false) let bindings = ComposerBindings(focused: false)
switch self { switch self {
case .send: viewModel = ComposerViewModel(initialViewState: ComposerViewState(textFormattingEnabled: true, bindings: bindings)) case .send: viewModel = ComposerViewModel(initialViewState: ComposerViewState(textFormattingEnabled: true, isLandscapePhone: false, bindings: bindings))
case .edit: viewModel = ComposerViewModel(initialViewState: ComposerViewState(sendMode: .edit, textFormattingEnabled: true, bindings: bindings)) case .edit: viewModel = ComposerViewModel(initialViewState: ComposerViewState(sendMode: .edit, textFormattingEnabled: true, isLandscapePhone: false, bindings: bindings))
case .reply: viewModel = ComposerViewModel(initialViewState: ComposerViewState(eventSenderDisplayName: "TestUser", sendMode: .reply, textFormattingEnabled: true, bindings: bindings)) case .reply: viewModel = ComposerViewModel(initialViewState: ComposerViewState(eventSenderDisplayName: "TestUser", sendMode: .reply, textFormattingEnabled: true, isLandscapePhone: false, bindings: bindings))
} }
let wysiwygviewModel = WysiwygComposerViewModel(minHeight: 20, maxCompressedHeight: 360) let wysiwygviewModel = WysiwygComposerViewModel(minHeight: 20, maxCompressedHeight: 360)

View file

@ -34,8 +34,8 @@ struct FormatItem {
enum FormatType { enum FormatType {
case bold case bold
case italic case italic
case strikethrough
case underline case underline
case strikethrough
} }
extension FormatType: CaseIterable, Identifiable { extension FormatType: CaseIterable, Identifiable {

View file

@ -20,6 +20,7 @@ struct ComposerViewState: BindableState {
var eventSenderDisplayName: String? var eventSenderDisplayName: String?
var sendMode: ComposerSendMode = .send var sendMode: ComposerSendMode = .send
var textFormattingEnabled: Bool var textFormattingEnabled: Bool
var isLandscapePhone: Bool
var placeholder: String? var placeholder: String?
var bindings: ComposerBindings var bindings: ComposerBindings
@ -47,6 +48,10 @@ extension ComposerViewState {
default: return nil default: return nil
} }
} }
var isMinimiseForced: Bool {
isLandscapePhone || !textFormattingEnabled
}
} }
struct ComposerBindings { struct ComposerBindings {

View file

@ -23,8 +23,13 @@ final class ComposerViewModelTests: XCTestCase {
var context: ComposerViewModel.Context! var context: ComposerViewModel.Context!
override func setUpWithError() throws { override func setUpWithError() throws {
viewModel = ComposerViewModel(initialViewState: ComposerViewState(textFormattingEnabled: true, viewModel = ComposerViewModel(
bindings: ComposerBindings(focused: false))) initialViewState: ComposerViewState(
textFormattingEnabled: true,
isLandscapePhone: false,
bindings: ComposerBindings(focused: false)
)
)
context = viewModel.context context = viewModel.context
} }

View file

@ -14,7 +14,6 @@
// limitations under the License. // limitations under the License.
// //
import DSBottomSheet
import SwiftUI import SwiftUI
import WysiwygComposer import WysiwygComposer
@ -22,7 +21,6 @@ struct Composer: View {
// MARK: - Properties // MARK: - Properties
// MARK: Private // MARK: Private
@ObservedObject private var viewModel: ComposerViewModelType.Context @ObservedObject private var viewModel: ComposerViewModelType.Context
@ObservedObject private var wysiwygViewModel: WysiwygComposerViewModel @ObservedObject private var wysiwygViewModel: WysiwygComposerViewModel
private let resizeAnimationDuration: Double private let resizeAnimationDuration: Double
@ -36,9 +34,8 @@ struct Composer: View {
private let horizontalPadding: CGFloat = 12 private let horizontalPadding: CGFloat = 12
private let borderHeight: CGFloat = 40 private let borderHeight: CGFloat = 40
private let minTextViewHeight: CGFloat = 22
private var verticalPadding: CGFloat { private var verticalPadding: CGFloat {
(borderHeight - minTextViewHeight) / 2 (borderHeight - wysiwygViewModel.minHeight) / 2
} }
private var topPadding: CGFloat { private var topPadding: CGFloat {
@ -46,7 +43,7 @@ struct Composer: View {
} }
private var cornerRadius: CGFloat { private var cornerRadius: CGFloat {
if viewModel.viewState.shouldDisplayContext || wysiwygViewModel.idealHeight > minTextViewHeight { if viewModel.viewState.shouldDisplayContext || wysiwygViewModel.idealHeight > wysiwygViewModel.minHeight {
return 14 return 14
} else { } else {
return borderHeight / 2 return borderHeight / 2
@ -119,7 +116,7 @@ struct Composer: View {
wysiwygViewModel.setup() wysiwygViewModel.setup()
} }
} }
if viewModel.viewState.textFormattingEnabled { if !viewModel.viewState.isMinimiseForced {
Button { Button {
wysiwygViewModel.maximised.toggle() wysiwygViewModel.maximised.toggle()
} label: { } label: {
@ -233,6 +230,11 @@ struct Composer: View {
} }
.padding(.horizontal, horizontalPadding) .padding(.horizontal, horizontalPadding)
.padding(.bottom, 4) .padding(.bottom, 4)
.onChange(of: viewModel.viewState.isMinimiseForced) { newValue in
if wysiwygViewModel.maximised && newValue {
wysiwygViewModel.maximised = false
}
}
} }
} }

View file

@ -63,6 +63,15 @@ final class ComposerViewModel: ComposerViewModelType, ComposerViewModelProtocol
} }
} }
var isLandscapePhone: Bool {
get {
state.isLandscapePhone
}
set {
state.isLandscapePhone = newValue
}
}
var isFocused: Bool { var isFocused: Bool {
state.bindings.focused state.bindings.focused
} }

View file

@ -24,6 +24,7 @@ protocol ComposerViewModelProtocol {
var eventSenderDisplayName: String? { get set } var eventSenderDisplayName: String? { get set }
var placeholder: String? { get set } var placeholder: String? { get set }
var isFocused: Bool { get } var isFocused: Bool { get }
var isLandscapePhone: Bool { get set }
func dismissKeyboard() func dismissKeyboard()
func showKeyboard() func showKeyboard()

View file

@ -34,7 +34,13 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol {
private var chunkFile: AVAudioFile! = nil private var chunkFile: AVAudioFile! = nil
private var chunkFrames: AVAudioFrameCount = 0 private var chunkFrames: AVAudioFrameCount = 0
private var chunkFileNumber: Int = 0 private var chunkFileNumber: Int = 0
private var currentElapsedTime: UInt = 0 // Time in seconds.
private var currentRemainingTime: UInt { // Time in seconds.
BuildSettings.voiceBroadcastMaxLength - currentElapsedTime
}
private var elapsedTimeTimer: Timer?
// MARK: Public // MARK: Public
weak var serviceDelegate: VoiceBroadcastRecorderServiceDelegate? weak var serviceDelegate: VoiceBroadcastRecorderServiceDelegate?
@ -67,12 +73,14 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol {
} }
try audioEngine.start() try audioEngine.start()
startTimer()
// Disable the sleep mode during the recording until we are able to handle it // Disable the sleep mode during the recording until we are able to handle it
UIApplication.shared.isIdleTimerDisabled = true UIApplication.shared.isIdleTimerDisabled = true
} catch { } catch {
MXLog.debug("[VoiceBroadcastRecorderService] startRecordingVoiceBroadcast error", context: error) MXLog.debug("[VoiceBroadcastRecorderService] startRecordingVoiceBroadcast error", context: error)
stopRecordingVoiceBroadcast() stopRecordingVoiceBroadcast()
invalidateTimer()
} }
} }
@ -81,6 +89,7 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol {
audioEngine.stop() audioEngine.stop()
audioEngine.inputNode.removeTap(onBus: audioNodeBus) audioEngine.inputNode.removeTap(onBus: audioNodeBus)
UIApplication.shared.isIdleTimerDisabled = false UIApplication.shared.isIdleTimerDisabled = false
invalidateTimer()
voiceBroadcastService?.stopVoiceBroadcast(success: { [weak self] _ in voiceBroadcastService?.stopVoiceBroadcast(success: { [weak self] _ in
MXLog.debug("[VoiceBroadcastRecorderService] Stopped") MXLog.debug("[VoiceBroadcastRecorderService] Stopped")
@ -110,6 +119,7 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol {
func pauseRecordingVoiceBroadcast() { func pauseRecordingVoiceBroadcast() {
audioEngine.pause() audioEngine.pause()
UIApplication.shared.isIdleTimerDisabled = false UIApplication.shared.isIdleTimerDisabled = false
invalidateTimer()
voiceBroadcastService?.pauseVoiceBroadcast(success: { [weak self] _ in voiceBroadcastService?.pauseVoiceBroadcast(success: { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
@ -126,6 +136,7 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol {
func resumeRecordingVoiceBroadcast() { func resumeRecordingVoiceBroadcast() {
try? audioEngine.start() try? audioEngine.start()
startTimer()
voiceBroadcastService?.resumeVoiceBroadcast(success: { [weak self] _ in voiceBroadcastService?.resumeVoiceBroadcast(success: { [weak self] _ in
guard let self = self else { return } guard let self = self else { return }
@ -143,12 +154,14 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol {
private func resetValues() { private func resetValues() {
chunkFrames = 0 chunkFrames = 0
chunkFileNumber = 0 chunkFileNumber = 0
currentElapsedTime = 0
} }
/// Release the service /// Release the service
private func tearDownVoiceBroadcastService() { private func tearDownVoiceBroadcastService() {
resetValues() resetValues()
session.tearDownVoiceBroadcastService() session.tearDownVoiceBroadcastService()
invalidateTimer()
do { do {
try AVAudioSession.sharedInstance().setActive(false) try AVAudioSession.sharedInstance().setActive(false)
@ -157,6 +170,31 @@ class VoiceBroadcastRecorderService: VoiceBroadcastRecorderServiceProtocol {
} }
} }
/// Start ElapsedTimeTimer.
private func startTimer() {
elapsedTimeTimer = Timer.scheduledTimer(timeInterval: 1.0,
target: self,
selector: #selector(updateCurrentElapsedTimeValue),
userInfo: nil,
repeats: true)
}
/// Invalidate ElapsedTimeTimer.
private func invalidateTimer() {
elapsedTimeTimer?.invalidate()
elapsedTimeTimer = nil
}
/// Update currentElapsedTime value.
@objc private func updateCurrentElapsedTimeValue() {
guard currentRemainingTime > 0 else {
stopRecordingVoiceBroadcast()
return
}
currentElapsedTime += 1
serviceDelegate?.voiceBroadcastRecorderService(self, didUpdateRemainingTime: self.currentRemainingTime)
}
/// Write audio buffer to chunk file. /// Write audio buffer to chunk file.
private func writeBuffer(_ buffer: AVAudioPCMBuffer) { private func writeBuffer(_ buffer: AVAudioPCMBuffer) {
let sampleRate = buffer.format.sampleRate let sampleRate = buffer.format.sampleRate

View file

@ -18,6 +18,7 @@ import Foundation
protocol VoiceBroadcastRecorderServiceDelegate: AnyObject { protocol VoiceBroadcastRecorderServiceDelegate: AnyObject {
func voiceBroadcastRecorderService(_ service: VoiceBroadcastRecorderServiceProtocol, didUpdateState state: VoiceBroadcastRecorderState) func voiceBroadcastRecorderService(_ service: VoiceBroadcastRecorderServiceProtocol, didUpdateState state: VoiceBroadcastRecorderState)
func voiceBroadcastRecorderService(_ service: VoiceBroadcastRecorderServiceProtocol, didUpdateRemainingTime remainingTime: UInt)
} }
protocol VoiceBroadcastRecorderServiceProtocol { protocol VoiceBroadcastRecorderServiceProtocol {

View file

@ -53,6 +53,14 @@ struct VoiceBroadcastRecorderView: View {
} icon: { } icon: {
Image(uiImage: Asset.Images.voiceBroadcastTileLive.image) Image(uiImage: Asset.Images.voiceBroadcastTileLive.image)
} }
Label {
Text(viewModel.viewState.currentRecordingState.remainingTimeLabel)
.foregroundColor(theme.colors.secondaryContent)
.font(theme.fonts.caption1)
} icon: {
Image(uiImage: Asset.Images.voiceBroadcastTimeLeft.image)
}
}.frame(maxWidth: .infinity, alignment: .leading) }.frame(maxWidth: .infinity, alignment: .leading)
Label { Label {

View file

@ -35,9 +35,15 @@ struct VoiceBroadcastRecorderDetails {
let avatarData: AvatarInputProtocol let avatarData: AvatarInputProtocol
} }
struct VoiceBroadcastRecordingState {
var remainingTime: UInt
var remainingTimeLabel: String
}
struct VoiceBroadcastRecorderViewState: BindableState { struct VoiceBroadcastRecorderViewState: BindableState {
var details: VoiceBroadcastRecorderDetails var details: VoiceBroadcastRecorderDetails
var recordingState: VoiceBroadcastRecorderState var recordingState: VoiceBroadcastRecorderState
var currentRecordingState: VoiceBroadcastRecordingState
var bindings: VoiceBroadcastRecorderViewStateBindings var bindings: VoiceBroadcastRecorderViewStateBindings
} }

View file

@ -32,7 +32,8 @@ enum MockVoiceBroadcastRecorderScreenState: MockScreenState, CaseIterable {
var screenView: ([Any], AnyView) { var screenView: ([Any], AnyView) {
let details = VoiceBroadcastRecorderDetails(senderDisplayName: "", avatarData: AvatarInput(mxContentUri: "", matrixItemId: "!fakeroomid:matrix.org", displayName: "The name of the room")) let details = VoiceBroadcastRecorderDetails(senderDisplayName: "", avatarData: AvatarInput(mxContentUri: "", matrixItemId: "!fakeroomid:matrix.org", displayName: "The name of the room"))
let viewModel = MockVoiceBroadcastRecorderViewModel(initialViewState: VoiceBroadcastRecorderViewState(details: details, recordingState: .started, bindings: VoiceBroadcastRecorderViewStateBindings())) let recordingState = VoiceBroadcastRecordingState(remainingTime: BuildSettings.voiceBroadcastMaxLength, remainingTimeLabel: "1h 20m 47s left")
let viewModel = MockVoiceBroadcastRecorderViewModel(initialViewState: VoiceBroadcastRecorderViewState(details: details, recordingState: .started, currentRecordingState: recordingState, bindings: VoiceBroadcastRecorderViewStateBindings()))
return ( return (
[false, viewModel], [false, viewModel],

View file

@ -34,8 +34,10 @@ class VoiceBroadcastRecorderViewModel: VoiceBroadcastRecorderViewModelType, Voic
init(details: VoiceBroadcastRecorderDetails, init(details: VoiceBroadcastRecorderDetails,
recorderService: VoiceBroadcastRecorderServiceProtocol) { recorderService: VoiceBroadcastRecorderServiceProtocol) {
self.voiceBroadcastRecorderService = recorderService self.voiceBroadcastRecorderService = recorderService
let currentRecordingState = VoiceBroadcastRecorderViewModel.currentRecordingState(from: BuildSettings.voiceBroadcastMaxLength)
super.init(initialViewState: VoiceBroadcastRecorderViewState(details: details, super.init(initialViewState: VoiceBroadcastRecorderViewState(details: details,
recordingState: .stopped, recordingState: .stopped,
currentRecordingState: currentRecordingState,
bindings: VoiceBroadcastRecorderViewStateBindings())) bindings: VoiceBroadcastRecorderViewStateBindings()))
self.voiceBroadcastRecorderService.serviceDelegate = self self.voiceBroadcastRecorderService.serviceDelegate = self
@ -77,10 +79,27 @@ class VoiceBroadcastRecorderViewModel: VoiceBroadcastRecorderViewModelType, Voic
self.state.recordingState = .resumed self.state.recordingState = .resumed
voiceBroadcastRecorderService.resumeRecordingVoiceBroadcast() voiceBroadcastRecorderService.resumeRecordingVoiceBroadcast()
} }
private func updateRemainingTime(_ remainingTime: UInt) {
state.currentRecordingState = VoiceBroadcastRecorderViewModel.currentRecordingState(from: remainingTime)
}
private static func currentRecordingState(from remainingTime: UInt) -> VoiceBroadcastRecordingState {
let time = TimeInterval(Double(remainingTime))
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .abbreviated
return VoiceBroadcastRecordingState(remainingTime: remainingTime,
remainingTimeLabel: VectorL10n.voiceBroadcastTimeLeft(formatter.string(from: time) ?? "0s"))
}
} }
extension VoiceBroadcastRecorderViewModel: VoiceBroadcastRecorderServiceDelegate { extension VoiceBroadcastRecorderViewModel: VoiceBroadcastRecorderServiceDelegate {
func voiceBroadcastRecorderService(_ service: VoiceBroadcastRecorderServiceProtocol, didUpdateState state: VoiceBroadcastRecorderState) { func voiceBroadcastRecorderService(_ service: VoiceBroadcastRecorderServiceProtocol, didUpdateState state: VoiceBroadcastRecorderState) {
self.state.recordingState = state self.state.recordingState = state
} }
func voiceBroadcastRecorderService(_ service: VoiceBroadcastRecorderServiceProtocol, didUpdateRemainingTime remainingTime: UInt) {
self.updateRemainingTime(remainingTime)
}
} }

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

@ -0,0 +1 @@
Labs: Rich text editor: Fix smart punctuation (e.g. double space transforms into dot)

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

@ -0,0 +1 @@
Labs: Rich text editor: Fix input for keyboards that use symbols composition and replacement (e.g. Japanese Romaji, Korean)

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

@ -0,0 +1 @@
Labs: Rich text editor: Fix keyboard suggestions for non-latin keyboards (e.g. Chinese Pinyin)

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

@ -0,0 +1 @@
Voice Messages: Fix crash when voice message finishes playing.

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

@ -0,0 +1 @@
Labs: Rich text editor: Fix broken backspace around some type of whitespaces

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

@ -0,0 +1 @@
Rich Text Editor: on iPhones when in landscape mode the fullscreen mode is disabled.

View file

@ -0,0 +1,2 @@
Loading: Display sync progress on the loading screen

View file

@ -0,0 +1 @@
Add the left time in the Voice Broadcast tile recorder.

View file

@ -0,0 +1 @@
Fix scroll issues with VoiceBroadcast and Poll cells

View file

@ -0,0 +1 @@
Refactor bottom sheet presentation in the device manager.

View file

@ -53,7 +53,7 @@ packages:
branch: main branch: main
WysiwygComposer: WysiwygComposer:
url: https://github.com/matrix-org/matrix-wysiwyg-composer-swift url: https://github.com/matrix-org/matrix-wysiwyg-composer-swift
revision: 2469f27b7e1e51aaa135e09f9005eb10fda686e6 revision: 1fbffd0321eb47abcd664ad19c6c943b60abf399
DeviceKit: DeviceKit:
url: https://github.com/devicekit/DeviceKit url: https://github.com/devicekit/DeviceKit
majorVersion: 4.7.0 majorVersion: 4.7.0