Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions NextcloudTalk.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,7 @@
NCMessageLocationParameter.m,
NCMessageParameter.m,
NCPoll.m,
ScheduledMessage.swift,
);
target = 2CC0014E24A1F0E900A20167 /* NotificationServiceExtension */;
};
Expand Down Expand Up @@ -569,6 +570,7 @@
NCMessageLocationParameter.m,
NCMessageParameter.m,
NCPoll.m,
ScheduledMessage.swift,
);
target = 2C62AFA224C08845007E460A /* ShareExtension */;
};
Expand All @@ -589,6 +591,7 @@
NCMessageLocationParameter.m,
NCMessageParameter.m,
NCPoll.m,
ScheduledMessage.swift,
);
target = 1FF2FD5A2AB99CCB000C9905 /* BroadcastUploadExtension */;
};
Expand All @@ -609,6 +612,7 @@
NCMessageLocationParameter.m,
NCMessageParameter.m,
NCPoll.m,
ScheduledMessage.swift,
);
target = 1FA93D9B2D70FCC200DF6CDF /* TalkIntents */;
};
Expand Down
73 changes: 68 additions & 5 deletions NextcloudTalk/Chat/BaseChatViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -704,7 +704,13 @@ import SwiftUI

func showVoiceMessageRecordButton() {
self.rightButton.setTitle("", for: .normal)
self.rightButton.setImage(UIImage(systemName: "mic"), for: .normal)

if self.room.hasScheduledMessages {
self.rightButton.setImage(UIImage(systemName: "clock"), for: .normal)
} else {
self.rightButton.setImage(UIImage(systemName: "mic"), for: .normal)
}

self.rightButton.tag = sendButtonTagVoice
self.rightButton.accessibilityLabel = NSLocalizedString("Record voice message", comment: "")
self.rightButton.accessibilityHint = NSLocalizedString("Tap and hold to record a voice message", comment: "")
Expand Down Expand Up @@ -738,6 +744,40 @@ import SwiftUI
let messageParameters = NCMessageParameter.messageParametersJSONString(from: self.mentionsDict) ?? ""
self.sendChatMessage(message: self.textView.text, withParentMessage: replyToMessage, messageParameters: messageParameters, silently: silently)

self.clearInputAfterSend()
}

func sendCurrentMessageLater(silently: Bool) {
Task { @MainActor in
let startingDate = Calendar.current.date(byAdding: .hour, value: 1, to: Date())
let minimumDate = Calendar.current.date(byAdding: .minute, value: 15, to: Date())

self.datePickerTextField.setupDatePicker(startingDate: startingDate, minimumDate: minimumDate)

let (buttonTapped, selectedDate) = await self.datePickerTextField.getDate()
guard buttonTapped == .done, let selectedDate else { return }

do {
let timestamp = Int(selectedDate.timeIntervalSince1970)
var replyToMessage: NCChatMessage?

if let replyMessageView, replyMessageView.isVisible {
replyToMessage = replyMessageView.message
}

try await NCAPIController.sharedInstance().scheduleMessage(self.textView.text, inRoom: self.room.token, sendAt: timestamp, replyTo: replyToMessage?.messageId, silent: silently, threadId: self.thread?.threadId, forAccount: self.account)
NotificationPresenter.shared().present(text: NSLocalizedString("Message successfully scheduled", comment: ""), dismissAfterDelay: 5.0, includedStyle: .success)

self.clearInputAfterSend()
NCRoomsManager.sharedInstance().updateRoom(self.room.token, withCompletionBlock: nil)
} catch {
print(error)
NotificationPresenter.shared().present(text: NSLocalizedString("Message scheduling failed", comment: ""), dismissAfterDelay: 5.0, includedStyle: .error)
}
}
}

private func clearInputAfterSend() {
self.mentionsDict.removeAll()
self.replyMessageView?.dismiss()
super.didPressRightButton(self)
Expand All @@ -753,7 +793,12 @@ import SwiftUI
self.sendCurrentMessage(silently: false)
super.didPressRightButton(sender)
case sendButtonTagVoice:
self.showVoiceMessageRecordHint()
if self.room.hasScheduledMessages {
let scheduledViewController = ScheduledMessagesChatViewController(forRoom: self.room, withAccount: self.account)!
self.presentWithNavigation(scheduledViewController, animated: true)
} else {
self.showVoiceMessageRecordHint()
}
default:
break
}
Expand All @@ -779,11 +824,23 @@ import SwiftUI
self.voiceMessageLongPressGesture = nil
}

let silentSendAction = UIAction(title: NSLocalizedString("Send without notification", comment: ""), image: UIImage(systemName: "bell.slash")) { [unowned self] _ in
self.sendCurrentMessage(silently: true)
var actions: [UIMenuElement] = []

if NCDatabaseManager.sharedInstance().serverHasTalkCapability(kCapabilityScheduleMessages, forAccountId: self.account.accountId) {
actions.append(UIAction(title: NSLocalizedString("Send later without notification", comment: ""), image: UIImage(named: "custom.paperplane.badge.clock")) { [unowned self] _ in
self.sendCurrentMessageLater(silently: true)
})

actions.append(UIAction(title: NSLocalizedString("Send later", comment: ""), image: UIImage(named: "custom.paperplane.badge.clock")) { [unowned self] _ in
self.sendCurrentMessageLater(silently: false)
})
}

self.rightButton.menu = UIMenu(children: [silentSendAction])
actions.append(UIAction(title: NSLocalizedString("Send without notification", comment: ""), image: UIImage(systemName: "bell.slash")) { [unowned self] _ in
self.sendCurrentMessage(silently: true)
})

self.rightButton.menu = UIMenu(children: actions)
}

func addMenuToLeftButton() {
Expand Down Expand Up @@ -1738,6 +1795,7 @@ import SwiftUI
self.recordCancelled = true
self.stopRecordingVoiceMessage()
handleCollapseVoiceRecording()
self.showVoiceMessageRecordButton()
}

func handleSend() {
Expand Down Expand Up @@ -1842,7 +1900,9 @@ import SwiftUI
}

func shareVoiceMessage() {
self.showVoiceMessageRecordButton()
guard let recorder = self.recorder else { return }

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH-mm-ss"
let dateString = dateFormatter.string(from: Date())
Expand Down Expand Up @@ -1960,6 +2020,8 @@ import SwiftUI
if flag, recorder == self.recorder, !self.recordCancelled {
self.shareVoiceMessage()
}

self.showVoiceMessageRecordButton()
}

// MARK: - Voice Messages Transcribe
Expand Down Expand Up @@ -2129,6 +2191,7 @@ import SwiftUI
self.longPressStartingPoint = point
self.cancelHintLabelInitialPositionX = voiceMessageRecordingView?.slideToCancelHintLabel?.frame.origin.x
self.voiceRecordingLockButton.alpha = 1
self.rightButton.setImage(UIImage(systemName: "mic"), for: .normal)
} else if gestureRecognizer.state == .ended {
self.shouldLockInterfaceOrientation(lock: false)
self.resetVoiceRecordingLockButton()
Expand Down
1 change: 1 addition & 0 deletions NextcloudTalk/Chat/Chat views/NCChatTitleView.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
@property (strong, nonatomic) UILongPressGestureRecognizer *longPressGestureRecognizer;

- (void)updateForRoom:(NCRoom *)room;
- (void)updateForScheduledMessagesIn:(NCRoom *)room;
- (void)updateForThread:(NCThread *)thread;

@end
5 changes: 5 additions & 0 deletions NextcloudTalk/Chat/Chat views/NCChatTitleView.m
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ - (void)updateForRoom:(NCRoom *)room
[self setTitle:room.displayName withSubtitle:subtitle];
}

- (void)updateForScheduledMessagesIn:(NCRoom *)room
{
[self setTitle:NSLocalizedString(@"Scheduled messages", nil) withSubtitle:room.displayName];
}

- (void)updateForThread:(NCThread *)thread
{
// Set thread image
Expand Down
17 changes: 4 additions & 13 deletions NextcloudTalk/Chat/ChatViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,6 @@ import SwiftUI

private var lobbyCheckTimer: Timer?

public var isThreadViewController: Bool {
return thread != nil
}

// MARK: - Thread notification levels

enum NotificationLevelOption: Int, CaseIterable {
Expand Down Expand Up @@ -511,15 +507,6 @@ import SwiftUI

private var messageExpirationTimer: Timer?

override func setTitleView() {
super.setTitleView()

if isThreadViewController {
self.titleView?.update(for: thread)
self.titleView?.longPressGestureRecognizer.isEnabled = false
}
}

public override init?(forRoom room: NCRoom, withAccount account: TalkAccount) {
self.chatController = NCChatController(for: room)

Expand Down Expand Up @@ -1322,6 +1309,10 @@ import SwiftUI
}

self.checkRetention()

// Update right button after receiving a room update (for scheduled messages)
// TODO: Button update should be a separate method
_ = self.canPressRightButton()
}

func didJoinRoom(notification: Notification) {
Expand Down
4 changes: 0 additions & 4 deletions NextcloudTalk/Chat/ContextChatViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ import Foundation
override func setTitleView() {
super.setTitleView()

if thread != nil {
self.titleView?.update(for: thread)
}

self.titleView?.longPressGestureRecognizer.isEnabled = false
}

Expand Down
12 changes: 11 additions & 1 deletion NextcloudTalk/Chat/InputbarViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ import UIKit
internal var contentView: UIView?
internal var selectedAutocompletionRow: IndexPath?

public var isThreadViewController: Bool {
return thread != nil
}

public init?(forRoom room: NCRoom, withAccount account: TalkAccount, tableViewStyle style: UITableView.Style) {
self.room = room
self.account = account
Expand Down Expand Up @@ -233,7 +237,13 @@ import UIKit
titleView.showSubtitle = false
}

titleView.update(for: self.room)
if isThreadViewController {
titleView.update(for: thread)
titleView.longPressGestureRecognizer.isEnabled = false
} else {
titleView.update(for: self.room)
}

self.titleView = titleView
self.navigationItem.titleView = titleView
}
Expand Down
65 changes: 65 additions & 0 deletions NextcloudTalk/Chat/ScheduledMessage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
// SPDX-License-Identifier: GPL-3.0-or-later
//

import Foundation

public class ScheduledMessage {

public var id: String
public var actor: TalkActor?
public var threadId: Int = 0
public var message: String
public var messageType: String
public var parentMessage: NCChatMessage?
public var silent: Bool
public var createdAtTimestamp: Int
public var sendAtTimestamp: Int

private var account: TalkAccount

init?(dictionary dict: [String: Any]?, withAccount account: TalkAccount) {
guard let dict else { return nil }

self.account = account

self.id = dict["id"] as? String ?? ""
self.threadId = dict["threadId"] as? Int ?? 0
self.message = dict["message"] as? String ?? ""
self.messageType = dict["messageType"] as? String ?? ""
self.silent = dict["silent"] as? Bool ?? false
self.createdAtTimestamp = dict["createdAt"] as? Int ?? 0
self.sendAtTimestamp = dict["sendAt"] as? Int ?? 0

if let actorId = dict["actorId"] as? String, let actorType = dict["actorType"] as? String {
self.actor = TalkActor(actorId: actorId, actorType: actorType)
}

if let parentMessage = dict["parent"] as? [String: Any] {
self.parentMessage = NCChatMessage(dictionary: parentMessage, andAccountId: account.accountId)
}
}

public func asChatMessage() -> NCChatMessage {
let message = NCChatMessage()

message.messageId = Int(self.id) ?? 0
message.actorId = self.account.userId
message.actorType = "users"
message.actorDisplayName = account.userDisplayName
message.accountId = self.account.accountId

message.timestamp = self.sendAtTimestamp
message.message = self.message
message.isSilent = self.silent

if let parentMessage {
message.parentId = parentMessage.internalId
}

message.threadId = self.threadId

return message
}
}
Loading
Loading