Skip to content

Commit

Permalink
Merge branch 'nextcloud:master' into support-videos-in-media-view
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielStandfest authored Oct 17, 2024
2 parents aba5377 + 914799e commit ca4522b
Show file tree
Hide file tree
Showing 90 changed files with 3,279 additions and 3,797 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/uitests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ permissions:

env:
WORKSPACE: NextcloudTalk.xcworkspace
DESTINATION: platform=iOS Simulator,name=iPhone 14,OS=16.2
DESTINATION: platform=iOS Simulator,name=iPhone 16,OS=18.0
SCHEME: NextcloudTalk

jobs:
build:
name: Build
runs-on: macos-12
runs-on: macos-15

steps:
- name: Checkout app
Expand Down Expand Up @@ -74,7 +74,7 @@ jobs:

test:
name: Test
runs-on: macos-12
runs-on: macos-15
needs: [build]

strategy:
Expand Down
182 changes: 154 additions & 28 deletions NextcloudTalk.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions NextcloudTalk/AVRoutePickerViewExtension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
// SPDX-License-Identifier: GPL-3.0-or-later
//

import UIKit
import AVKit

extension AVRoutePickerView {
func showPicker() {
self.subviews.compactMap { $0 as? UIButton } .forEach { $0.sendActions(for: .touchUpInside )}
}
}
2 changes: 2 additions & 0 deletions NextcloudTalk/AppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@

#import "NextcloudTalk-Swift.h"

@import UICKeyChainStore;

@interface AppDelegate ()

@property (nonatomic, strong) NSTimer *keepAliveTimer;
Expand Down
115 changes: 115 additions & 0 deletions NextcloudTalk/AudioPlayerView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
//
// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
// SPDX-License-Identifier: GPL-3.0-or-later
//

import UIKit

protocol AudioPlayerViewDelegate: AnyObject {

func audioPlayerPlayButtonPressed()
func audioPlayerPauseButtonPressed()
func audioPlayerProgressChanged(progress: CGFloat)
}

class AudioPlayerView: UIView {

@IBOutlet var contentView: UIView!
@IBOutlet weak var playPauseButton: UIButton!
@IBOutlet weak var slider: UISlider!
@IBOutlet weak var durationLabel: UILabel!

var isPlaying: Bool = false

public weak var delegate: AudioPlayerViewDelegate?

override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}

func commonInit() {
Bundle.main.loadNibNamed("AudioPlayerView", owner: self, options: nil)

contentView.frame = self.bounds
contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]

// Play/Pause button
playPauseButton.addAction(for: .touchUpInside) { [unowned self] in
if isPlaying {
delegate?.audioPlayerPauseButtonPressed()
} else {
delegate?.audioPlayerPlayButtonPressed()
}
}

// Slider
let thumbImage = UIImage(systemName: "circle.fill")?
.withConfiguration(UIImage.SymbolConfiguration(pointSize: 16))
.withTintColor(.label, renderingMode: .alwaysOriginal)
slider.setThumbImage(thumbImage, for: .normal)
slider.semanticContentAttribute = .forceLeftToRight
slider.addAction(for: .valueChanged) { [unowned self] in
delegate?.audioPlayerProgressChanged(progress: CGFloat(slider.value))
}

// Duration label
hideDurationLabel()

backgroundColor = .secondarySystemBackground
layer.cornerRadius = 8.0
layer.masksToBounds = true

self.addSubview(contentView)
}

func setPlayerProgress(_ progress: CGFloat, isPlaying playing: Bool, maximumValue maxValue: CGFloat) {
setPlayPauseButton(playing: playing)
slider.isEnabled = true
slider.value = Float(progress)
slider.maximumValue = Float(maxValue)
setDurationLabel(progress: progress, duration: maxValue)
slider.setNeedsLayout()
}

func resetPlayer() {
setPlayPauseButton(playing: false)
slider.isEnabled = false
slider.value = 0
hideDurationLabel()
slider.setNeedsLayout()
}

func setPlayPauseButton(playing: Bool) {
isPlaying = playing

if isPlaying {
playPauseButton.setImage(UIImage(systemName: "pause.fill"), for: .normal)
} else {
playPauseButton.setImage(UIImage(systemName: "play.fill"), for: .normal)
}
}

func setDurationLabel(progress: CGFloat, duration: CGFloat) {
let dateComponentsFormatter = DateComponentsFormatter()
dateComponentsFormatter.allowedUnits = [.minute, .second]
dateComponentsFormatter.zeroFormattingBehavior = []

let progressTime = dateComponentsFormatter.string(from: TimeInterval(progress)) ?? "0:00"
let durationTime = dateComponentsFormatter.string(from: TimeInterval(duration)) ?? "0:00"

let playerTimeString = "\(progressTime)".withTextColor(.label).withFont(.systemFont(ofSize: 13, weight: .medium))
playerTimeString.append(" / \(durationTime)".withTextColor(.secondaryLabel).withFont(.systemFont(ofSize: 13)))

durationLabel.attributedText = playerTimeString
}

func hideDurationLabel() {
durationLabel.text = ""
}
}
67 changes: 67 additions & 0 deletions NextcloudTalk/AudioPlayerView.xib
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="23094" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23084"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="AudioPlayerView" customModule="NextcloudTalk" customModuleProvider="target">
<connections>
<outlet property="contentView" destination="iN0-l3-epB" id="WHP-2s-6Il"/>
<outlet property="durationLabel" destination="7vy-DL-v2I" id="gLr-z8-lYD"/>
<outlet property="playPauseButton" destination="weF-VJ-kQg" id="6O8-WK-cv9"/>
<outlet property="slider" destination="Cgz-Rx-FyJ" id="TME-tZ-DL1"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB">
<rect key="frame" x="0.0" y="0.0" width="441" height="52"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="weF-VJ-kQg">
<rect key="frame" x="8" y="4" width="44" height="44"/>
<constraints>
<constraint firstAttribute="width" constant="44" id="TwB-9N-gEK"/>
<constraint firstAttribute="height" constant="44" id="XIa-Qd-GZD"/>
</constraints>
<color key="tintColor" systemColor="labelColor"/>
<state key="normal" title="Button"/>
<buttonConfiguration key="configuration" style="plain" image="play.fill" catalog="system"/>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="751" text="0:00 / 0:02" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7vy-DL-v2I">
<rect key="frame" x="366" y="0.0" width="67" height="52"/>
<fontDescription key="fontDescription" type="system" pointSize="13"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<slider opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="0.5" minValue="0.0" maxValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="Cgz-Rx-FyJ">
<rect key="frame" x="58" y="11" width="302" height="31"/>
</slider>
</subviews>
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
<constraints>
<constraint firstItem="Cgz-Rx-FyJ" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="0pD-pS-YhM"/>
<constraint firstAttribute="bottom" secondItem="7vy-DL-v2I" secondAttribute="bottom" id="1PB-ja-hxc"/>
<constraint firstItem="Cgz-Rx-FyJ" firstAttribute="leading" secondItem="weF-VJ-kQg" secondAttribute="trailing" constant="8" symbolic="YES" id="H3b-Y3-6vI"/>
<constraint firstItem="weF-VJ-kQg" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" constant="4" id="MAo-xn-Ott"/>
<constraint firstAttribute="bottom" secondItem="weF-VJ-kQg" secondAttribute="bottom" constant="4" id="Ms8-VQ-WhR"/>
<constraint firstItem="weF-VJ-kQg" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="8" id="QBV-dj-HsA"/>
<constraint firstItem="7vy-DL-v2I" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="QFI-XE-9Fd"/>
<constraint firstAttribute="trailing" secondItem="7vy-DL-v2I" secondAttribute="trailing" constant="8" id="VnI-wM-6MZ"/>
<constraint firstItem="7vy-DL-v2I" firstAttribute="leading" secondItem="Cgz-Rx-FyJ" secondAttribute="trailing" constant="8" id="nHt-Db-jET"/>
</constraints>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<point key="canvasLocation" x="130.53435114503816" y="-233.09859154929578"/>
</view>
</objects>
<resources>
<image name="play.fill" catalog="system" width="120" height="128"/>
<systemColor name="labelColor">
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>
55 changes: 55 additions & 0 deletions NextcloudTalk/BaseChatTableViewCell+Audio.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
// SPDX-License-Identifier: GPL-3.0-or-later
//

extension BaseChatTableViewCell {

func setupForAudioCell(with message: NCChatMessage) {
if self.audioPlayerView == nil {
// Audio player view
let audioPlayerView = AudioPlayerView(frame: CGRect(x: 0, y: 0, width: 0, height: voiceMessageCellPlayerHeight))
self.audioPlayerView = audioPlayerView
self.audioPlayerView?.delegate = self

audioPlayerView.translatesAutoresizingMaskIntoConstraints = false

self.messageBodyView.addSubview(audioPlayerView)

NSLayoutConstraint.activate([
audioPlayerView.leftAnchor.constraint(equalTo: self.messageBodyView.leftAnchor),
audioPlayerView.rightAnchor.constraint(equalTo: self.messageBodyView.rightAnchor),
audioPlayerView.topAnchor.constraint(equalTo: self.messageBodyView.topAnchor)
])
}
}

func prepareForReuseAudioCell() {
self.audioPlayerView?.resetPlayer()
self.clearFileStatusView()
}

func audioPlayerPlayButtonPressed() {
guard let audioFileParameter = message?.file() else {
return
}

self.delegate?.cellWants(toPlayAudioFile: audioFileParameter)
}

func audioPlayerPauseButtonPressed() {
guard let audioFileParameter = message?.file() else {
return
}

self.delegate?.cellWants(toPauseAudioFile: audioFileParameter)
}

func audioPlayerProgressChanged(progress: CGFloat) {
guard let audioFileParameter = message?.file() else {
return
}

self.delegate?.cellWants(toChangeProgress: progress, fromAudioFile: audioFileParameter)
}
}
68 changes: 0 additions & 68 deletions NextcloudTalk/BaseChatTableViewCell+File.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,6 @@ extension BaseChatTableViewCell {
messageTextView.topAnchor.constraint(equalTo: filePreviewImageView.bottomAnchor, constant: 10),
messageTextView.bottomAnchor.constraint(equalTo: self.messageBodyView.bottomAnchor)
])

NotificationCenter.default.addObserver(self, selector: #selector(didChangeIsDownloading(notification:)), name: NSNotification.Name.NCChatFileControllerDidChangeIsDownloading, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(didChangeDownloadProgress(notification:)), name: NSNotification.Name.NCChatFileControllerDidChangeDownloadProgress, object: nil)
}

guard let filePreviewImageView = self.filePreviewImageView,
Expand Down Expand Up @@ -305,69 +302,4 @@ extension BaseChatTableViewCell {

return ceil(previewSize.height)
}

// MARK: - File status / activity indicator

func clearFileStatusView() {
self.fileActivityIndicator?.stopAnimating()
self.fileActivityIndicator?.removeFromSuperview()
self.fileActivityIndicator = nil
}

func addActivityIndicator(with progress: Float) {
self.clearFileStatusView()

let fileActivityIndicator = MDCActivityIndicator(frame: .init(x: 0, y: 0, width: 20, height: 20))
self.fileActivityIndicator = fileActivityIndicator

fileActivityIndicator.radius = 7
fileActivityIndicator.cycleColors = [.systemGray2]

if progress > 0 {
fileActivityIndicator.indicatorMode = .determinate
fileActivityIndicator.setProgress(progress, animated: false)
}

fileActivityIndicator.startAnimating()
fileActivityIndicator.heightAnchor.constraint(equalToConstant: 20).isActive = true
self.statusView.addArrangedSubview(fileActivityIndicator)
}

// MARK: - File notifications

@objc func didChangeIsDownloading(notification: Notification) {
DispatchQueue.main.async {
// Make sure this notification is really for this cell
guard let fileParameter = self.message?.file(),
let receivedStatus = NCChatFileStatus.getStatus(from: notification, for: fileParameter)
else { return }

if receivedStatus.isDownloading, self.fileActivityIndicator == nil {
// Immediately show an indeterminate indicator as long as we don't have a progress value
self.addActivityIndicator(with: 0)
} else if !receivedStatus.isDownloading, self.fileActivityIndicator != nil {
self.clearFileStatusView()
}
}
}

@objc func didChangeDownloadProgress(notification: Notification) {
DispatchQueue.main.async {
// Make sure this notification is really for this cell
guard let fileParameter = self.message?.file(),
let receivedStatus = NCChatFileStatus.getStatus(from: notification, for: fileParameter)
else { return }

if self.fileActivityIndicator != nil {
// Switch to determinate-mode and show progress
if receivedStatus.canReportProgress {
self.fileActivityIndicator?.indicatorMode = .determinate
self.fileActivityIndicator?.setProgress(Float(receivedStatus.downloadProgress), animated: true)
}
} else {
// Make sure we have an activity indicator added to this cell
self.addActivityIndicator(with: Float(receivedStatus.downloadProgress))
}
}
}
}
Loading

0 comments on commit ca4522b

Please sign in to comment.