mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-28 23:32:41 +00:00
Merge branch 'develop' into alfogrillo/badges_spaces
This commit is contained in:
commit
9e2e2d03a3
44 changed files with 298 additions and 69 deletions
1
.github/workflows/release-alpha.yml
vendored
1
.github/workflows/release-alpha.yml
vendored
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
12
Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_time_left.imageset/Contents.json
vendored
Normal file
12
Riot/Assets/Images.xcassets/VoiceBroadcast/voice_broadcast_time_left.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "voice_broadcast_time_left.svg",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 |
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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] {
|
||||||
|
|
|
@ -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()
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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],
|
||||||
|
|
|
@ -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
1
changelog.d/6930.bugfix
Normal 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
1
changelog.d/6983.bugfix
Normal 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
1
changelog.d/7042.bugfix
Normal 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
1
changelog.d/7074.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Voice Messages: Fix crash when voice message finishes playing.
|
1
changelog.d/7086.bugfix
Normal file
1
changelog.d/7086.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Labs: Rich text editor: Fix broken backspace around some type of whitespaces
|
1
changelog.d/7096.change
Normal file
1
changelog.d/7096.change
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Rich Text Editor: on iPhones when in landscape mode the fullscreen mode is disabled.
|
2
changelog.d/pr-7101.change
Normal file
2
changelog.d/pr-7101.change
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Loading: Display sync progress on the loading screen
|
||||||
|
|
1
changelog.d/pr-7103.feature
Normal file
1
changelog.d/pr-7103.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Add the left time in the Voice Broadcast tile recorder.
|
1
changelog.d/pr-7105.bugfix
Normal file
1
changelog.d/pr-7105.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Fix scroll issues with VoiceBroadcast and Poll cells
|
1
changelog.d/pr-7107.change
Normal file
1
changelog.d/pr-7107.change
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Refactor bottom sheet presentation in the device manager.
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue