mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-29 04:39:53 +00:00
ios: allow to set disappearance interval when sending message; allow to configure custom interval (#2428)
* ios: allow to set disappearance interval when sending message; allow to configure custom interval * custom time picker wip * improve interaction with time picker - touch area, cancel, keyboard, preference * dropdown picker, refactor, text * button condition * weeks limit * refactor * update texts * simplify * fix null selection * texts, set current, switch columns --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
parent
d62761b3a8
commit
25156bb56c
12 changed files with 442 additions and 112 deletions
|
@ -301,9 +301,9 @@ func apiGetChatItemInfo(itemId: Int64) async throws -> ChatItemInfo {
|
||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent, live: Bool = false) async -> ChatItem? {
|
func apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent, live: Bool = false, ttl: Int? = nil) async -> ChatItem? {
|
||||||
let chatModel = ChatModel.shared
|
let chatModel = ChatModel.shared
|
||||||
let cmd: ChatCommand = .apiSendMessage(type: type, id: id, file: file, quotedItemId: quotedItemId, msg: msg, live: live)
|
let cmd: ChatCommand = .apiSendMessage(type: type, id: id, file: file, quotedItemId: quotedItemId, msg: msg, live: live, ttl: ttl)
|
||||||
let r: ChatResponse
|
let r: ChatResponse
|
||||||
if type == .direct {
|
if type == .direct {
|
||||||
var cItem: ChatItem!
|
var cItem: ChatItem!
|
||||||
|
|
|
@ -32,7 +32,7 @@ func ciMetaText(_ meta: CIMeta, chatTTL: Int?, color: Color = .clear, transparen
|
||||||
r = r + statusIconText("timer", color).font(.caption2)
|
r = r + statusIconText("timer", color).font(.caption2)
|
||||||
let ttl = meta.itemTimed?.ttl
|
let ttl = meta.itemTimed?.ttl
|
||||||
if ttl != chatTTL {
|
if ttl != chatTTL {
|
||||||
r = r + Text(TimedMessagesPreference.shortTtlText(ttl)).foregroundColor(color)
|
r = r + Text(shortTimeText(ttl)).foregroundColor(color)
|
||||||
}
|
}
|
||||||
r = r + Text(" ")
|
r = r + Text(" ")
|
||||||
}
|
}
|
||||||
|
|
|
@ -277,8 +277,8 @@ struct ComposeView: View {
|
||||||
ZStack(alignment: .leading) {
|
ZStack(alignment: .leading) {
|
||||||
SendMessageView(
|
SendMessageView(
|
||||||
composeState: $composeState,
|
composeState: $composeState,
|
||||||
sendMessage: {
|
sendMessage: { ttl in
|
||||||
sendMessage()
|
sendMessage(ttl: ttl)
|
||||||
resetLinkPreview()
|
resetLinkPreview()
|
||||||
},
|
},
|
||||||
sendLiveMessage: sendLiveMessage,
|
sendLiveMessage: sendLiveMessage,
|
||||||
|
@ -296,6 +296,9 @@ struct ComposeView: View {
|
||||||
},
|
},
|
||||||
finishVoiceMessageRecording: finishVoiceMessageRecording,
|
finishVoiceMessageRecording: finishVoiceMessageRecording,
|
||||||
allowVoiceMessagesToContact: allowVoiceMessagesToContact,
|
allowVoiceMessagesToContact: allowVoiceMessagesToContact,
|
||||||
|
// TODO in 5.2 - allow if ttl is not configured
|
||||||
|
// timedMessageAllowed: chat.chatInfo.featureEnabled(.timedMessages),
|
||||||
|
timedMessageAllowed: chat.chatInfo.featureEnabled(.timedMessages) && chat.chatInfo.timedMessagesTTL != nil,
|
||||||
onMediaAdded: { media in if !media.isEmpty { chosenMedia = media }},
|
onMediaAdded: { media in if !media.isEmpty { chosenMedia = media }},
|
||||||
keyboardVisible: $keyboardVisible
|
keyboardVisible: $keyboardVisible
|
||||||
)
|
)
|
||||||
|
@ -425,7 +428,7 @@ struct ComposeView: View {
|
||||||
&& (!composeState.message.isEmpty || composeState.liveMessage?.sentMsg != nil) {
|
&& (!composeState.message.isEmpty || composeState.liveMessage?.sentMsg != nil) {
|
||||||
cancelCurrentVoiceRecording()
|
cancelCurrentVoiceRecording()
|
||||||
clearCurrentDraft()
|
clearCurrentDraft()
|
||||||
sendMessage()
|
sendMessage(ttl: nil)
|
||||||
resetLinkPreview()
|
resetLinkPreview()
|
||||||
} else if (composeState.inProgress) {
|
} else if (composeState.inProgress) {
|
||||||
clearCurrentDraft()
|
clearCurrentDraft()
|
||||||
|
@ -470,7 +473,7 @@ struct ComposeView: View {
|
||||||
let lm = composeState.liveMessage
|
let lm = composeState.liveMessage
|
||||||
if (composeState.sendEnabled || composeState.quoting)
|
if (composeState.sendEnabled || composeState.quoting)
|
||||||
&& (lm == nil || lm?.sentMsg == nil),
|
&& (lm == nil || lm?.sentMsg == nil),
|
||||||
let ci = await sendMessageAsync(typedMsg, live: true) {
|
let ci = await sendMessageAsync(typedMsg, live: true, ttl: nil) {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
composeState = composeState.copy(liveMessage: LiveMessage(chatItem: ci, typedMsg: typedMsg, sentMsg: typedMsg))
|
composeState = composeState.copy(liveMessage: LiveMessage(chatItem: ci, typedMsg: typedMsg, sentMsg: typedMsg))
|
||||||
}
|
}
|
||||||
|
@ -486,7 +489,7 @@ struct ComposeView: View {
|
||||||
let typedMsg = composeState.message
|
let typedMsg = composeState.message
|
||||||
if let liveMessage = composeState.liveMessage {
|
if let liveMessage = composeState.liveMessage {
|
||||||
if let sentMsg = liveMessageToSend(liveMessage, typedMsg),
|
if let sentMsg = liveMessageToSend(liveMessage, typedMsg),
|
||||||
let ci = await sendMessageAsync(sentMsg, live: true) {
|
let ci = await sendMessageAsync(sentMsg, live: true, ttl: nil) {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
composeState = composeState.copy(liveMessage: LiveMessage(chatItem: ci, typedMsg: typedMsg, sentMsg: sentMsg))
|
composeState = composeState.copy(liveMessage: LiveMessage(chatItem: ci, typedMsg: typedMsg, sentMsg: sentMsg))
|
||||||
}
|
}
|
||||||
|
@ -578,15 +581,15 @@ struct ComposeView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func sendMessage() {
|
private func sendMessage(ttl: Int?) {
|
||||||
logger.debug("ChatView sendMessage")
|
logger.debug("ChatView sendMessage")
|
||||||
Task {
|
Task {
|
||||||
logger.debug("ChatView sendMessage: in Task")
|
logger.debug("ChatView sendMessage: in Task")
|
||||||
_ = await sendMessageAsync(nil, live: false)
|
_ = await sendMessageAsync(nil, live: false, ttl: ttl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func sendMessageAsync(_ text: String?, live: Bool) async -> ChatItem? {
|
private func sendMessageAsync(_ text: String?, live: Bool, ttl: Int?) async -> ChatItem? {
|
||||||
var sent: ChatItem?
|
var sent: ChatItem?
|
||||||
let msgText = text ?? composeState.message
|
let msgText = text ?? composeState.message
|
||||||
let liveMessage = composeState.liveMessage
|
let liveMessage = composeState.liveMessage
|
||||||
|
@ -606,36 +609,36 @@ struct ComposeView: View {
|
||||||
|
|
||||||
switch (composeState.preview) {
|
switch (composeState.preview) {
|
||||||
case .noPreview:
|
case .noPreview:
|
||||||
sent = await send(.text(msgText), quoted: quoted, live: live)
|
sent = await send(.text(msgText), quoted: quoted, live: live, ttl: ttl)
|
||||||
case .linkPreview:
|
case .linkPreview:
|
||||||
sent = await send(checkLinkPreview(), quoted: quoted, live: live)
|
sent = await send(checkLinkPreview(), quoted: quoted, live: live, ttl: ttl)
|
||||||
case let .mediaPreviews(mediaPreviews: media):
|
case let .mediaPreviews(mediaPreviews: media):
|
||||||
let last = media.count - 1
|
let last = media.count - 1
|
||||||
if last >= 0 {
|
if last >= 0 {
|
||||||
for i in 0..<last {
|
for i in 0..<last {
|
||||||
if case (_, .video(_, _, _)) = media[i] {
|
if case (_, .video(_, _, _)) = media[i] {
|
||||||
sent = await sendVideo(media[i])
|
sent = await sendVideo(media[i], ttl: ttl)
|
||||||
} else {
|
} else {
|
||||||
sent = await sendImage(media[i])
|
sent = await sendImage(media[i], ttl: ttl)
|
||||||
}
|
}
|
||||||
_ = try? await Task.sleep(nanoseconds: 100_000000)
|
_ = try? await Task.sleep(nanoseconds: 100_000000)
|
||||||
}
|
}
|
||||||
if case (_, .video(_, _, _)) = media[last] {
|
if case (_, .video(_, _, _)) = media[last] {
|
||||||
sent = await sendVideo(media[last], text: msgText, quoted: quoted, live: live)
|
sent = await sendVideo(media[last], text: msgText, quoted: quoted, live: live, ttl: ttl)
|
||||||
} else {
|
} else {
|
||||||
sent = await sendImage(media[last], text: msgText, quoted: quoted, live: live)
|
sent = await sendImage(media[last], text: msgText, quoted: quoted, live: live, ttl: ttl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if sent == nil {
|
if sent == nil {
|
||||||
sent = await send(.text(msgText), quoted: quoted, live: live)
|
sent = await send(.text(msgText), quoted: quoted, live: live, ttl: ttl)
|
||||||
}
|
}
|
||||||
case let .voicePreview(recordingFileName, duration):
|
case let .voicePreview(recordingFileName, duration):
|
||||||
stopPlayback.toggle()
|
stopPlayback.toggle()
|
||||||
chatModel.filesToDelete.remove(getAppFilePath(recordingFileName))
|
chatModel.filesToDelete.remove(getAppFilePath(recordingFileName))
|
||||||
sent = await send(.voice(text: msgText, duration: duration), quoted: quoted, file: recordingFileName)
|
sent = await send(.voice(text: msgText, duration: duration), quoted: quoted, file: recordingFileName, ttl: ttl)
|
||||||
case let .filePreview(_, file):
|
case let .filePreview(_, file):
|
||||||
if let savedFile = saveFileFromURL(file) {
|
if let savedFile = saveFileFromURL(file) {
|
||||||
sent = await send(.file(msgText), quoted: quoted, file: savedFile, live: live)
|
sent = await send(.file(msgText), quoted: quoted, file: savedFile, live: live, ttl: ttl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -691,30 +694,31 @@ struct ComposeView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendImage(_ imageData: (String, UploadContent?), text: String = "", quoted: Int64? = nil, live: Bool = false) async -> ChatItem? {
|
func sendImage(_ imageData: (String, UploadContent?), text: String = "", quoted: Int64? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? {
|
||||||
let (image, data) = imageData
|
let (image, data) = imageData
|
||||||
if let data = data, let savedFile = saveAnyImage(data) {
|
if let data = data, let savedFile = saveAnyImage(data) {
|
||||||
return await send(.image(text: text, image: image), quoted: quoted, file: savedFile, live: live)
|
return await send(.image(text: text, image: image), quoted: quoted, file: savedFile, live: live, ttl: ttl)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendVideo(_ imageData: (String, UploadContent?), text: String = "", quoted: Int64? = nil, live: Bool = false) async -> ChatItem? {
|
func sendVideo(_ imageData: (String, UploadContent?), text: String = "", quoted: Int64? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? {
|
||||||
let (image, data) = imageData
|
let (image, data) = imageData
|
||||||
if case let .video(_, url, duration) = data, let savedFile = saveFileFromURLWithoutLoad(url) {
|
if case let .video(_, url, duration) = data, let savedFile = saveFileFromURLWithoutLoad(url) {
|
||||||
return await send(.video(text: text, image: image, duration: duration), quoted: quoted, file: savedFile, live: live)
|
return await send(.video(text: text, image: image, duration: duration), quoted: quoted, file: savedFile, live: live, ttl: ttl)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func send(_ mc: MsgContent, quoted: Int64?, file: String? = nil, live: Bool = false) async -> ChatItem? {
|
func send(_ mc: MsgContent, quoted: Int64?, file: String? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? {
|
||||||
if let chatItem = await apiSendMessage(
|
if let chatItem = await apiSendMessage(
|
||||||
type: chat.chatInfo.chatType,
|
type: chat.chatInfo.chatType,
|
||||||
id: chat.chatInfo.apiId,
|
id: chat.chatInfo.apiId,
|
||||||
file: file,
|
file: file,
|
||||||
quotedItemId: quoted,
|
quotedItemId: quoted,
|
||||||
msg: mc,
|
msg: mc,
|
||||||
live: live
|
live: live,
|
||||||
|
ttl: ttl
|
||||||
) {
|
) {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
chatModel.removeLiveDummy(animated: false)
|
chatModel.removeLiveDummy(animated: false)
|
||||||
|
|
|
@ -13,7 +13,7 @@ private let liveMsgInterval: UInt64 = 3000_000000
|
||||||
|
|
||||||
struct SendMessageView: View {
|
struct SendMessageView: View {
|
||||||
@Binding var composeState: ComposeState
|
@Binding var composeState: ComposeState
|
||||||
var sendMessage: () -> Void
|
var sendMessage: (Int?) -> Void
|
||||||
var sendLiveMessage: (() async -> Void)? = nil
|
var sendLiveMessage: (() async -> Void)? = nil
|
||||||
var updateLiveMessage: (() async -> Void)? = nil
|
var updateLiveMessage: (() async -> Void)? = nil
|
||||||
var cancelLiveMessage: (() -> Void)? = nil
|
var cancelLiveMessage: (() -> Void)? = nil
|
||||||
|
@ -23,6 +23,7 @@ struct SendMessageView: View {
|
||||||
var startVoiceMessageRecording: (() -> Void)? = nil
|
var startVoiceMessageRecording: (() -> Void)? = nil
|
||||||
var finishVoiceMessageRecording: (() -> Void)? = nil
|
var finishVoiceMessageRecording: (() -> Void)? = nil
|
||||||
var allowVoiceMessagesToContact: (() -> Void)? = nil
|
var allowVoiceMessagesToContact: (() -> Void)? = nil
|
||||||
|
var timedMessageAllowed: Bool = false
|
||||||
var onMediaAdded: ([UploadContent]) -> Void
|
var onMediaAdded: ([UploadContent]) -> Void
|
||||||
@State private var holdingVMR = false
|
@State private var holdingVMR = false
|
||||||
@Namespace var namespace
|
@Namespace var namespace
|
||||||
|
@ -32,6 +33,9 @@ struct SendMessageView: View {
|
||||||
@State private var teUiFont: UIFont = UIFont.preferredFont(forTextStyle: .body)
|
@State private var teUiFont: UIFont = UIFont.preferredFont(forTextStyle: .body)
|
||||||
@State private var sendButtonSize: CGFloat = 29
|
@State private var sendButtonSize: CGFloat = 29
|
||||||
@State private var sendButtonOpacity: CGFloat = 1
|
@State private var sendButtonOpacity: CGFloat = 1
|
||||||
|
@State private var showCustomDisappearingMessageDialogue = false
|
||||||
|
@State private var showCustomTimePicker = false
|
||||||
|
@State private var selectedDisappearingMessageTime: Int? = customDisappearingMessageTimeDefault.get()
|
||||||
var maxHeight: CGFloat = 360
|
var maxHeight: CGFloat = 360
|
||||||
var minHeight: CGFloat = 37
|
var minHeight: CGFloat = 37
|
||||||
@AppStorage(DEFAULT_LIVE_MESSAGE_ALERT_SHOWN) private var liveMessageAlertShown = false
|
@AppStorage(DEFAULT_LIVE_MESSAGE_ALERT_SHOWN) private var liveMessageAlertShown = false
|
||||||
|
@ -147,15 +151,17 @@ struct SendMessageView: View {
|
||||||
.padding([.top, .trailing], 4)
|
.padding([.top, .trailing], 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func sendMessageButton() -> some View {
|
private func sendMessageButton() -> some View {
|
||||||
let v = Button(action: sendMessage) {
|
Button {
|
||||||
|
sendMessage(nil)
|
||||||
|
} label: {
|
||||||
Image(systemName: composeState.editing || composeState.liveMessage != nil
|
Image(systemName: composeState.editing || composeState.liveMessage != nil
|
||||||
? "checkmark.circle.fill"
|
? "checkmark.circle.fill"
|
||||||
: "arrow.up.circle.fill")
|
: "arrow.up.circle.fill")
|
||||||
.resizable()
|
.resizable()
|
||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
.frame(width: sendButtonSize, height: sendButtonSize)
|
.frame(width: sendButtonSize, height: sendButtonSize)
|
||||||
.opacity(sendButtonOpacity)
|
.opacity(sendButtonOpacity)
|
||||||
}
|
}
|
||||||
.disabled(
|
.disabled(
|
||||||
!composeState.sendEnabled ||
|
!composeState.sendEnabled ||
|
||||||
|
@ -164,22 +170,61 @@ struct SendMessageView: View {
|
||||||
composeState.endLiveDisabled
|
composeState.endLiveDisabled
|
||||||
)
|
)
|
||||||
.frame(width: 29, height: 29)
|
.frame(width: 29, height: 29)
|
||||||
|
.contextMenu{
|
||||||
|
sendButtonContextMenuItems()
|
||||||
|
}
|
||||||
|
.padding([.bottom, .trailing], 4)
|
||||||
|
.confirmationDialog("Send disappearing message", isPresented: $showCustomDisappearingMessageDialogue, titleVisibility: .visible) {
|
||||||
|
Button("30 seconds") { sendMessage(30) }
|
||||||
|
Button("1 minute") { sendMessage(60) }
|
||||||
|
Button("5 minutes") { sendMessage(300) }
|
||||||
|
Button("Custom time") { showCustomTimePicker = true }
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showCustomTimePicker, onDismiss: { selectedDisappearingMessageTime = customDisappearingMessageTimeDefault.get() }) {
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
disappearingMessageCustomTimePicker()
|
||||||
|
.presentationDetents([.fraction(0.6)])
|
||||||
|
} else {
|
||||||
|
disappearingMessageCustomTimePicker()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func disappearingMessageCustomTimePicker() -> some View {
|
||||||
|
CustomTimePickerView(
|
||||||
|
selection: $selectedDisappearingMessageTime,
|
||||||
|
confirmButtonText: "Send",
|
||||||
|
confirmButtonAction: {
|
||||||
|
if let time = selectedDisappearingMessageTime {
|
||||||
|
sendMessage(time)
|
||||||
|
customDisappearingMessageTimeDefault.set(time)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
description: "Delete after"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private func sendButtonContextMenuItems() -> some View {
|
||||||
if composeState.liveMessage == nil,
|
if composeState.liveMessage == nil,
|
||||||
case .noContextItem = composeState.contextItem,
|
!composeState.editing {
|
||||||
!composeState.voicePreview && !composeState.editing,
|
if case .noContextItem = composeState.contextItem,
|
||||||
let send = sendLiveMessage,
|
!composeState.voicePreview,
|
||||||
let update = updateLiveMessage {
|
let send = sendLiveMessage,
|
||||||
v.contextMenu{
|
let update = updateLiveMessage {
|
||||||
Button {
|
Button {
|
||||||
startLiveMessage(send: send, update: update)
|
startLiveMessage(send: send, update: update)
|
||||||
} label: {
|
} label: {
|
||||||
Label("Send live message", systemImage: "bolt.fill")
|
Label("Send live message", systemImage: "bolt.fill")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding([.bottom, .trailing], 4)
|
if timedMessageAllowed {
|
||||||
} else {
|
Button {
|
||||||
v.padding([.bottom, .trailing], 4)
|
hideKeyboard()
|
||||||
|
showCustomDisappearingMessageDialogue = true
|
||||||
|
} label: {
|
||||||
|
Label("Disappearing message", systemImage: "stopwatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -365,7 +410,7 @@ struct SendMessageView_Previews: PreviewProvider {
|
||||||
Spacer(minLength: 0)
|
Spacer(minLength: 0)
|
||||||
SendMessageView(
|
SendMessageView(
|
||||||
composeState: $composeStateNew,
|
composeState: $composeStateNew,
|
||||||
sendMessage: {},
|
sendMessage: { _ in },
|
||||||
onMediaAdded: { _ in },
|
onMediaAdded: { _ in },
|
||||||
keyboardVisible: $keyboardVisible
|
keyboardVisible: $keyboardVisible
|
||||||
)
|
)
|
||||||
|
@ -375,7 +420,7 @@ struct SendMessageView_Previews: PreviewProvider {
|
||||||
Spacer(minLength: 0)
|
Spacer(minLength: 0)
|
||||||
SendMessageView(
|
SendMessageView(
|
||||||
composeState: $composeStateEditing,
|
composeState: $composeStateEditing,
|
||||||
sendMessage: {},
|
sendMessage: { _ in },
|
||||||
onMediaAdded: { _ in },
|
onMediaAdded: { _ in },
|
||||||
keyboardVisible: $keyboardVisible
|
keyboardVisible: $keyboardVisible
|
||||||
)
|
)
|
||||||
|
|
|
@ -89,9 +89,16 @@ struct ContactPreferencesView: View {
|
||||||
}
|
}
|
||||||
infoRow("Contact allows", pref.contactPreference.allow.text)
|
infoRow("Contact allows", pref.contactPreference.allow.text)
|
||||||
if featuresAllowed.timedMessagesAllowed {
|
if featuresAllowed.timedMessagesAllowed {
|
||||||
timedMessagesTTLPicker($featuresAllowed.timedMessagesTTL)
|
DropdownCustomTimePicker(
|
||||||
|
selection: $featuresAllowed.timedMessagesTTL,
|
||||||
|
label: "Delete after",
|
||||||
|
dropdownValues: TimedMessagesPreference.ttlValues,
|
||||||
|
customPickerConfirmButtonText: "Select",
|
||||||
|
customPickerDescription: "Delete after"
|
||||||
|
)
|
||||||
|
.frame(height: 36)
|
||||||
} else if pref.contactPreference.allow == .yes || pref.contactPreference.allow == .always {
|
} else if pref.contactPreference.allow == .yes || pref.contactPreference.allow == .always {
|
||||||
infoRow("Delete after", TimedMessagesPreference.ttlText(pref.contactPreference.ttl))
|
infoRow("Delete after", timeText(pref.contactPreference.ttl))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
header: { featureHeader(.timedMessages, enabled) }
|
header: { featureHeader(.timedMessages, enabled) }
|
||||||
|
@ -129,18 +136,6 @@ struct ContactPreferencesView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func timedMessagesTTLPicker(_ selection: Binding<Int?>) -> some View {
|
|
||||||
Picker("Delete after", selection: selection) {
|
|
||||||
let selectedTTL = selection.wrappedValue
|
|
||||||
let ttlValues = TimedMessagesPreference.ttlValues
|
|
||||||
let values = ttlValues + (ttlValues.contains(selectedTTL) ? [] : [selectedTTL])
|
|
||||||
ForEach(values, id: \.self) { ttl in
|
|
||||||
Text(TimedMessagesPreference.ttlText(ttl))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(height: 36)
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ContactPreferencesView_Previews: PreviewProvider {
|
struct ContactPreferencesView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
ContactPreferencesView(
|
ContactPreferencesView(
|
||||||
|
|
|
@ -75,14 +75,21 @@ struct GroupPreferencesView: View {
|
||||||
Toggle(feature.text, isOn: enable)
|
Toggle(feature.text, isOn: enable)
|
||||||
}
|
}
|
||||||
if timedOn {
|
if timedOn {
|
||||||
timedMessagesTTLPicker($preferences.timedMessages.ttl)
|
DropdownCustomTimePicker(
|
||||||
|
selection: $preferences.timedMessages.ttl,
|
||||||
|
label: "Delete after",
|
||||||
|
dropdownValues: TimedMessagesPreference.ttlValues,
|
||||||
|
customPickerConfirmButtonText: "Select",
|
||||||
|
customPickerDescription: "Delete after"
|
||||||
|
)
|
||||||
|
.frame(height: 36)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
settingsRow(icon, color: color) {
|
settingsRow(icon, color: color) {
|
||||||
infoRow(Text(feature.text), enableFeature.wrappedValue.text)
|
infoRow(Text(feature.text), enableFeature.wrappedValue.text)
|
||||||
}
|
}
|
||||||
if timedOn {
|
if timedOn {
|
||||||
infoRow("Delete after", TimedMessagesPreference.ttlText(preferences.timedMessages.ttl))
|
infoRow("Delete after", timeText(preferences.timedMessages.ttl))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} footer: {
|
} footer: {
|
||||||
|
|
223
apps/ios/Shared/Views/Helpers/CustomTimePicker.swift
Normal file
223
apps/ios/Shared/Views/Helpers/CustomTimePicker.swift
Normal file
|
@ -0,0 +1,223 @@
|
||||||
|
//
|
||||||
|
// CustomTimePicker.swift
|
||||||
|
// SimpleX (iOS)
|
||||||
|
//
|
||||||
|
// Created by spaced4ndy on 11.05.2023.
|
||||||
|
// Copyright © 2023 SimpleX Chat. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import SimpleXChat
|
||||||
|
|
||||||
|
struct CustomTimePicker: View {
|
||||||
|
@Binding var selection: Int?
|
||||||
|
@State var timeUnitsLimits = TimeUnitLimits.defaultUnitsLimits
|
||||||
|
@State private var selectedUnit: CustomTimeUnit = .second
|
||||||
|
@State private var selectedDuration: Int = 1
|
||||||
|
|
||||||
|
struct TimeUnitLimits {
|
||||||
|
var timeUnit: CustomTimeUnit
|
||||||
|
var minValue: Int = 1
|
||||||
|
var maxValue: Int
|
||||||
|
|
||||||
|
public static func defaultUnitLimits(_ unit: CustomTimeUnit) -> TimeUnitLimits {
|
||||||
|
switch unit {
|
||||||
|
case .second: return TimeUnitLimits.init(timeUnit: .second, maxValue: 120)
|
||||||
|
case .minute: return TimeUnitLimits.init(timeUnit: .minute, maxValue: 120)
|
||||||
|
case .hour: return TimeUnitLimits.init(timeUnit: .hour, maxValue: 72)
|
||||||
|
case .day: return TimeUnitLimits.init(timeUnit: .day, maxValue: 30)
|
||||||
|
case .week: return TimeUnitLimits.init(timeUnit: .week, maxValue: 12)
|
||||||
|
case .month: return TimeUnitLimits.init(timeUnit: .month, maxValue: 3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var defaultUnitsLimits: [TimeUnitLimits] {[
|
||||||
|
defaultUnitLimits(.second),
|
||||||
|
defaultUnitLimits(.minute),
|
||||||
|
defaultUnitLimits(.hour),
|
||||||
|
defaultUnitLimits(.day),
|
||||||
|
defaultUnitLimits(.week),
|
||||||
|
defaultUnitLimits(.month),
|
||||||
|
]}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Group {
|
||||||
|
Picker("Duration", selection: $selectedDuration) {
|
||||||
|
let selectedUnitLimits = timeUnitsLimits.first(where: { $0.timeUnit == selectedUnit }) ?? TimeUnitLimits.defaultUnitLimits(selectedUnit)
|
||||||
|
let selectedUnitValues = Array(selectedUnitLimits.minValue...selectedUnitLimits.maxValue)
|
||||||
|
let values = selectedUnitValues + (selectedUnitValues.contains(selectedDuration) ? [] : [selectedDuration])
|
||||||
|
ForEach(values, id: \.self) { value in
|
||||||
|
Text("\(value)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Picker("Unit", selection: $selectedUnit) {
|
||||||
|
ForEach(timeUnitsLimits.map { $0.timeUnit }, id: \.self) { timeUnit in
|
||||||
|
Text(timeUnit.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(.wheel)
|
||||||
|
.frame(minWidth: 0)
|
||||||
|
.compositingGroup()
|
||||||
|
.clipped()
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
if let selection = selection,
|
||||||
|
selection > 0 {
|
||||||
|
(selectedUnit, selectedDuration) = CustomTimeUnit.toTimeUnit(seconds: selection)
|
||||||
|
} else {
|
||||||
|
selection = selectedUnit.toSeconds * selectedDuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: selectedUnit) { unit in
|
||||||
|
if let maxValue = timeUnitsLimits.first(where: { $0.timeUnit == unit })?.maxValue,
|
||||||
|
selectedDuration > maxValue {
|
||||||
|
selectedDuration = maxValue
|
||||||
|
} else {
|
||||||
|
selection = unit.toSeconds * selectedDuration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: selectedDuration) { duration in
|
||||||
|
selection = selectedUnit.toSeconds * duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UIPickerView {
|
||||||
|
open override var intrinsicContentSize: CGSize {
|
||||||
|
return CGSize(width: UIView.noIntrinsicMetric, height: super.intrinsicContentSize.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CustomTimePickerView: View {
|
||||||
|
@Environment(\.dismiss) var dismiss
|
||||||
|
@Binding var selection: Int?
|
||||||
|
var confirmButtonText: LocalizedStringKey
|
||||||
|
var confirmButtonAction: () -> Void
|
||||||
|
var description: LocalizedStringKey? = nil
|
||||||
|
var timeUnitsLimits = CustomTimePicker.TimeUnitLimits.defaultUnitsLimits
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationView {
|
||||||
|
customTimePickerView()
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
Button("Cancel") {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
Button {
|
||||||
|
confirmButtonAction()
|
||||||
|
dismiss()
|
||||||
|
} label: {
|
||||||
|
Text(confirmButtonText)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
}
|
||||||
|
.disabled(selection == nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func customTimePickerView() -> some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
List {
|
||||||
|
Group {
|
||||||
|
Section(description ?? "") {
|
||||||
|
CustomTimePicker(selection: $selection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listRowInsets(.init(top: 0, leading: 16, bottom: 0, trailing: 16))
|
||||||
|
}
|
||||||
|
.listStyle(.insetGrouped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DropdownCustomTimePicker: View {
|
||||||
|
@Binding var selection: Int?
|
||||||
|
var label: LocalizedStringKey
|
||||||
|
var dropdownValues: [Int?]
|
||||||
|
var customPickerConfirmButtonText: LocalizedStringKey
|
||||||
|
var customPickerDescription: LocalizedStringKey? = nil
|
||||||
|
var customPickerTimeUnitsLimits = CustomTimePicker.TimeUnitLimits.defaultUnitsLimits
|
||||||
|
@State private var dropdownSelection: DropdownSelection = .dropdownValue(value: nil)
|
||||||
|
@State private var showCustomTimePicker = false
|
||||||
|
@State private var selectedCustomTime: Int? = nil
|
||||||
|
|
||||||
|
enum DropdownSelection: Hashable {
|
||||||
|
case dropdownValue(value: Int?)
|
||||||
|
case custom
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Picker(label, selection: $dropdownSelection) {
|
||||||
|
let values: [DropdownSelection] =
|
||||||
|
dropdownValues.map { .dropdownValue(value: $0) }
|
||||||
|
+ (dropdownValues.contains(selection) ? [] : [.dropdownValue(value: selection)])
|
||||||
|
+ [.custom]
|
||||||
|
ForEach(values, id: \.self) { v in
|
||||||
|
switch v {
|
||||||
|
case let .dropdownValue(value): Text(timeText(value))
|
||||||
|
case .custom: Text(NSLocalizedString("custom", comment: "dropdown time picker choice"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
dropdownSelection = .dropdownValue(value: selection)
|
||||||
|
}
|
||||||
|
.onChange(of: selection) { v in
|
||||||
|
logger.debug("*** .onChange(of: selection)")
|
||||||
|
dropdownSelection = .dropdownValue(value: v)
|
||||||
|
}
|
||||||
|
.onChange(of: dropdownSelection) { v in
|
||||||
|
logger.debug("*** .onChange(of: dropdownSelection)")
|
||||||
|
switch v {
|
||||||
|
case let .dropdownValue(value): selection = value
|
||||||
|
case .custom: showCustomTimePicker = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(
|
||||||
|
isPresented: $showCustomTimePicker,
|
||||||
|
onDismiss: {
|
||||||
|
dropdownSelection = .dropdownValue(value: selection)
|
||||||
|
selectedCustomTime = nil
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
customTimePicker()
|
||||||
|
.presentationDetents([.fraction(0.6)])
|
||||||
|
} else {
|
||||||
|
customTimePicker()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func customTimePicker() -> some View {
|
||||||
|
CustomTimePickerView(
|
||||||
|
selection: $selectedCustomTime,
|
||||||
|
confirmButtonText: customPickerConfirmButtonText,
|
||||||
|
confirmButtonAction: {
|
||||||
|
if let time = selectedCustomTime {
|
||||||
|
selection = time
|
||||||
|
}
|
||||||
|
},
|
||||||
|
description: customPickerDescription,
|
||||||
|
timeUnitsLimits: customPickerTimeUnitsLimits
|
||||||
|
)
|
||||||
|
.onAppear {
|
||||||
|
selectedCustomTime = selection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CustomTimePicker_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
CustomTimePicker(
|
||||||
|
selection: Binding.constant(300)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -72,7 +72,7 @@ struct TerminalView: View {
|
||||||
|
|
||||||
SendMessageView(
|
SendMessageView(
|
||||||
composeState: $composeState,
|
composeState: $composeState,
|
||||||
sendMessage: sendMessage,
|
sendMessage: { _ in consoleSendMessage() },
|
||||||
showVoiceMessageButton: false,
|
showVoiceMessageButton: false,
|
||||||
onMediaAdded: { _ in },
|
onMediaAdded: { _ in },
|
||||||
keyboardVisible: $keyboardVisible
|
keyboardVisible: $keyboardVisible
|
||||||
|
@ -108,7 +108,7 @@ struct TerminalView: View {
|
||||||
.onDisappear { terminalItem = nil }
|
.onDisappear { terminalItem = nil }
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendMessage() {
|
func consoleSendMessage() {
|
||||||
let cmd = ChatCommand.string(composeState.message)
|
let cmd = ChatCommand.string(composeState.message)
|
||||||
if composeState.message.starts(with: "/sql") && (!prefPerformLA || !developerTools) {
|
if composeState.message.starts(with: "/sql") && (!prefPerformLA || !developerTools) {
|
||||||
let resp = ChatResponse.chatCmdError(user_: nil, chatError: ChatError.error(errorType: ChatErrorType.commandError(message: "Failed reading: empty")))
|
let resp = ChatResponse.chatCmdError(user_: nil, chatError: ChatError.error(errorType: ChatErrorType.commandError(message: "Failed reading: empty")))
|
||||||
|
|
|
@ -48,6 +48,7 @@ let DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE = "showHiddenProfilesNotice"
|
||||||
let DEFAULT_SHOW_MUTE_PROFILE_ALERT = "showMuteProfileAlert"
|
let DEFAULT_SHOW_MUTE_PROFILE_ALERT = "showMuteProfileAlert"
|
||||||
let DEFAULT_WHATS_NEW_VERSION = "defaultWhatsNewVersion"
|
let DEFAULT_WHATS_NEW_VERSION = "defaultWhatsNewVersion"
|
||||||
let DEFAULT_ONBOARDING_STAGE = "onboardingStage"
|
let DEFAULT_ONBOARDING_STAGE = "onboardingStage"
|
||||||
|
let DEFAULT_CUSTOM_DISAPPEARING_MESSAGE_TIME = "customDisappearingMessageTime"
|
||||||
|
|
||||||
let appDefaults: [String: Any] = [
|
let appDefaults: [String: Any] = [
|
||||||
DEFAULT_SHOW_LA_NOTICE: false,
|
DEFAULT_SHOW_LA_NOTICE: false,
|
||||||
|
@ -76,6 +77,7 @@ let appDefaults: [String: Any] = [
|
||||||
DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE: true,
|
DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE: true,
|
||||||
DEFAULT_SHOW_MUTE_PROFILE_ALERT: true,
|
DEFAULT_SHOW_MUTE_PROFILE_ALERT: true,
|
||||||
DEFAULT_ONBOARDING_STAGE: OnboardingStage.onboardingComplete.rawValue,
|
DEFAULT_ONBOARDING_STAGE: OnboardingStage.onboardingComplete.rawValue,
|
||||||
|
DEFAULT_CUSTOM_DISAPPEARING_MESSAGE_TIME: 300,
|
||||||
]
|
]
|
||||||
|
|
||||||
enum SimpleXLinkMode: String, Identifiable {
|
enum SimpleXLinkMode: String, Identifiable {
|
||||||
|
@ -112,6 +114,8 @@ let privacyLocalAuthModeDefault = EnumDefault<LAMode>(defaults: UserDefaults.sta
|
||||||
|
|
||||||
let onboardingStageDefault = EnumDefault<OnboardingStage>(defaults: UserDefaults.standard, forKey: DEFAULT_ONBOARDING_STAGE, withDefault: .onboardingComplete)
|
let onboardingStageDefault = EnumDefault<OnboardingStage>(defaults: UserDefaults.standard, forKey: DEFAULT_ONBOARDING_STAGE, withDefault: .onboardingComplete)
|
||||||
|
|
||||||
|
let customDisappearingMessageTimeDefault = IntDefault(defaults: UserDefaults.standard, forKey: DEFAULT_CUSTOM_DISAPPEARING_MESSAGE_TIME)
|
||||||
|
|
||||||
func setGroupDefaults() {
|
func setGroupDefaults() {
|
||||||
privacyAcceptImagesGroupDefault.set(UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES))
|
privacyAcceptImagesGroupDefault.set(UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES))
|
||||||
}
|
}
|
||||||
|
|
|
@ -173,6 +173,7 @@
|
||||||
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; };
|
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; };
|
||||||
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; };
|
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; };
|
||||||
64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */; };
|
64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */; };
|
||||||
|
64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; };
|
||||||
64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; };
|
64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; };
|
||||||
64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; };
|
64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; };
|
||||||
64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */; };
|
64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */; };
|
||||||
|
@ -444,6 +445,7 @@
|
||||||
64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = "<group>"; };
|
64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = "<group>"; };
|
||||||
64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = "<group>"; };
|
64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = "<group>"; };
|
||||||
64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemInfoView.swift; sourceTree = "<group>"; };
|
64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemInfoView.swift; sourceTree = "<group>"; };
|
||||||
|
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = "<group>"; };
|
||||||
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = "<group>"; };
|
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = "<group>"; };
|
||||||
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = "<group>"; };
|
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = "<group>"; };
|
||||||
64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactLearnMore.swift; sourceTree = "<group>"; };
|
64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactLearnMore.swift; sourceTree = "<group>"; };
|
||||||
|
@ -612,6 +614,7 @@
|
||||||
5CCB939B297EFCB100399E78 /* NavStackCompat.swift */,
|
5CCB939B297EFCB100399E78 /* NavStackCompat.swift */,
|
||||||
18415DAAAD1ADBEDB0EDA852 /* VideoPlayerView.swift */,
|
18415DAAAD1ADBEDB0EDA852 /* VideoPlayerView.swift */,
|
||||||
64466DCB29FFE3E800E3D48D /* MailView.swift */,
|
64466DCB29FFE3E800E3D48D /* MailView.swift */,
|
||||||
|
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */,
|
||||||
);
|
);
|
||||||
path = Helpers;
|
path = Helpers;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1190,6 +1193,7 @@
|
||||||
1841538E296606C74533367C /* UserPicker.swift in Sources */,
|
1841538E296606C74533367C /* UserPicker.swift in Sources */,
|
||||||
18415B0585EB5A9A0A7CA8CD /* PressedButtonStyle.swift in Sources */,
|
18415B0585EB5A9A0A7CA8CD /* PressedButtonStyle.swift in Sources */,
|
||||||
1841560FD1CD447955474C1D /* UserProfilesView.swift in Sources */,
|
1841560FD1CD447955474C1D /* UserProfilesView.swift in Sources */,
|
||||||
|
64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */,
|
||||||
18415C6C56DBCEC2CBBD2F11 /* WebRTCClient.swift in Sources */,
|
18415C6C56DBCEC2CBBD2F11 /* WebRTCClient.swift in Sources */,
|
||||||
184152CEF68D2336FC2EBCB0 /* CallViewRenderers.swift in Sources */,
|
184152CEF68D2336FC2EBCB0 /* CallViewRenderers.swift in Sources */,
|
||||||
5CB634AD29E46CF70066AD6B /* LocalAuthView.swift in Sources */,
|
5CB634AD29E46CF70066AD6B /* LocalAuthView.swift in Sources */,
|
||||||
|
|
|
@ -37,7 +37,7 @@ public enum ChatCommand {
|
||||||
case apiGetChats(userId: Int64)
|
case apiGetChats(userId: Int64)
|
||||||
case apiGetChat(type: ChatType, id: Int64, pagination: ChatPagination, search: String)
|
case apiGetChat(type: ChatType, id: Int64, pagination: ChatPagination, search: String)
|
||||||
case apiGetChatItemInfo(itemId: Int64)
|
case apiGetChatItemInfo(itemId: Int64)
|
||||||
case apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent, live: Bool)
|
case apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent, live: Bool, ttl: Int?)
|
||||||
case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent, live: Bool)
|
case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent, live: Bool)
|
||||||
case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode)
|
case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode)
|
||||||
case apiDeleteMemberChatItem(groupId: Int64, groupMemberId: Int64, itemId: Int64)
|
case apiDeleteMemberChatItem(groupId: Int64, groupMemberId: Int64, itemId: Int64)
|
||||||
|
@ -141,9 +141,10 @@ public enum ChatCommand {
|
||||||
case let .apiGetChat(type, id, pagination, search): return "/_get chat \(ref(type, id)) \(pagination.cmdString)" +
|
case let .apiGetChat(type, id, pagination, search): return "/_get chat \(ref(type, id)) \(pagination.cmdString)" +
|
||||||
(search == "" ? "" : " search=\(search)")
|
(search == "" ? "" : " search=\(search)")
|
||||||
case let .apiGetChatItemInfo(itemId): return "/_get item info \(itemId)"
|
case let .apiGetChatItemInfo(itemId): return "/_get item info \(itemId)"
|
||||||
case let .apiSendMessage(type, id, file, quotedItemId, mc, live):
|
case let .apiSendMessage(type, id, file, quotedItemId, mc, live, ttl):
|
||||||
let msg = encodeJSON(ComposedMessage(filePath: file, quotedItemId: quotedItemId, msgContent: mc))
|
let msg = encodeJSON(ComposedMessage(filePath: file, quotedItemId: quotedItemId, msgContent: mc))
|
||||||
return "/_send \(ref(type, id)) live=\(onOff(live)) json \(msg)"
|
let ttlStr = ttl != nil ? "\(ttl!)" : "default"
|
||||||
|
return "/_send \(ref(type, id)) live=\(onOff(live)) ttl=\(ttlStr) json \(msg)"
|
||||||
case let .apiUpdateChatItem(type, id, itemId, mc, live): return "/_update item \(ref(type, id)) \(itemId) live=\(onOff(live)) \(mc.cmdString)"
|
case let .apiUpdateChatItem(type, id, itemId, mc, live): return "/_update item \(ref(type, id)) \(itemId) live=\(onOff(live)) \(mc.cmdString)"
|
||||||
case let .apiDeleteChatItem(type, id, itemId, mode): return "/_delete item \(ref(type, id)) \(itemId) \(mode.rawValue)"
|
case let .apiDeleteChatItem(type, id, itemId, mode): return "/_delete item \(ref(type, id)) \(itemId) \(mode.rawValue)"
|
||||||
case let .apiDeleteMemberChatItem(groupId, groupMemberId, itemId): return "/_delete member item #\(groupId) \(groupMemberId) \(itemId)"
|
case let .apiDeleteMemberChatItem(groupId, groupMemberId, itemId): return "/_delete member item #\(groupId) \(groupMemberId) \(itemId)"
|
||||||
|
|
|
@ -280,60 +280,107 @@ public struct TimedMessagesPreference: Preference {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var ttlValues: [Int?] {
|
public static var ttlValues: [Int?] {
|
||||||
[30, 300, 3600, 8 * 3600, 86400, 7 * 86400, 30 * 86400, nil]
|
[3600, 8 * 3600, 86400, 7 * 86400, 30 * 86400, nil]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum CustomTimeUnit {
|
||||||
|
case second
|
||||||
|
case minute
|
||||||
|
case hour
|
||||||
|
case day
|
||||||
|
case week
|
||||||
|
case month
|
||||||
|
|
||||||
|
public var toSeconds: Int {
|
||||||
|
switch self {
|
||||||
|
case .second: return 1
|
||||||
|
case .minute: return 60
|
||||||
|
case .hour: return 3600
|
||||||
|
case .day: return 86400
|
||||||
|
case .week: return 7 * 86400
|
||||||
|
case .month: return 30 * 86400
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func ttlText(_ ttl: Int?) -> String {
|
public var text: String {
|
||||||
guard let ttl = ttl else { return "off" }
|
switch self {
|
||||||
if ttl == 0 { return "0 sec" }
|
case .second: return NSLocalizedString("seconds", comment: "time unit")
|
||||||
let (m_, s) = divMod(ttl, by: 60)
|
case .minute: return NSLocalizedString("minutes", comment: "time unit")
|
||||||
let (h_, m) = divMod(m_, by: 60)
|
case .hour: return NSLocalizedString("hours", comment: "time unit")
|
||||||
let (d_, h) = divMod(h_, by: 24)
|
case .day: return NSLocalizedString("days", comment: "time unit")
|
||||||
let (mm, d) = divMod(d_, by: 30)
|
case .week: return NSLocalizedString("weeks", comment: "time unit")
|
||||||
return maybe(mm,
|
case .month: return NSLocalizedString("months", comment: "time unit")
|
||||||
mm == 1
|
}
|
||||||
? NSLocalizedString("1 month", comment: "message ttl")
|
|
||||||
: String.localizedStringWithFormat(NSLocalizedString("%d months", comment: "message ttl"), mm)
|
|
||||||
)
|
|
||||||
+ maybe(d,
|
|
||||||
d == 1
|
|
||||||
? NSLocalizedString("1 day", comment: "message ttl")
|
|
||||||
: d == 7
|
|
||||||
? NSLocalizedString("1 week", comment: "message ttl")
|
|
||||||
: d == 14
|
|
||||||
? NSLocalizedString("2 weeks", comment: "message ttl")
|
|
||||||
: String.localizedStringWithFormat(NSLocalizedString("%d days", comment: "message ttl"), d)
|
|
||||||
)
|
|
||||||
+ maybe(h,
|
|
||||||
h == 1
|
|
||||||
? NSLocalizedString("1 hour", comment: "message ttl")
|
|
||||||
: String.localizedStringWithFormat(NSLocalizedString("%d hours", comment: "message ttl"), h)
|
|
||||||
)
|
|
||||||
+ maybe(m, String.localizedStringWithFormat(NSLocalizedString("%d min", comment: "message ttl"), m))
|
|
||||||
+ maybe(s, String.localizedStringWithFormat(NSLocalizedString("%d sec", comment: "message ttl"), s))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func shortTtlText(_ ttl: Int?) -> LocalizedStringKey {
|
public static func toTimeUnit(seconds: Int) -> (CustomTimeUnit, Int) {
|
||||||
guard let ttl = ttl else { return "off" }
|
let tryUnits = [month, week, day, hour, minute]
|
||||||
let m = ttl / 60
|
var selectedUnit: (CustomTimeUnit, Int)? = nil
|
||||||
if m == 0 { return "\(ttl)s" }
|
for unit in tryUnits {
|
||||||
let h = m / 60
|
let (v, r) = divMod(seconds, by: unit.toSeconds)
|
||||||
if h == 0 { return "\(m)m" }
|
if r == 0 {
|
||||||
let d = h / 24
|
selectedUnit = (unit, v)
|
||||||
if d == 0 { return "\(h)h" }
|
break
|
||||||
let mm = d / 30
|
}
|
||||||
if mm > 0 { return "\(mm)mth" }
|
}
|
||||||
let w = d / 7
|
return selectedUnit ?? (CustomTimeUnit.second, seconds)
|
||||||
return w == 0 || d % 7 != 0 ? "\(d)d" : "\(w)w"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func divMod(_ n: Int, by d: Int) -> (Int, Int) {
|
private static func divMod(_ n: Int, by d: Int) -> (Int, Int) {
|
||||||
(n / d, n % d)
|
(n / d, n % d)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func maybe(_ n: Int, _ s: String) -> String {
|
public static func toText(seconds: Int) -> String {
|
||||||
n == 0 ? "" : s
|
let (unit, value) = toTimeUnit(seconds: seconds)
|
||||||
|
switch unit {
|
||||||
|
case .second:
|
||||||
|
return String.localizedStringWithFormat(NSLocalizedString("%d sec", comment: "time interval"), value)
|
||||||
|
case .minute:
|
||||||
|
return String.localizedStringWithFormat(NSLocalizedString("%d min", comment: "time interval"), value)
|
||||||
|
case .hour:
|
||||||
|
return value == 1
|
||||||
|
? NSLocalizedString("1 hour", comment: "time interval")
|
||||||
|
: String.localizedStringWithFormat(NSLocalizedString("%d hours", comment: "time interval"), value)
|
||||||
|
case .day:
|
||||||
|
return value == 1
|
||||||
|
? NSLocalizedString("1 day", comment: "time interval")
|
||||||
|
: String.localizedStringWithFormat(NSLocalizedString("%d days", comment: "time interval"), value)
|
||||||
|
case .week:
|
||||||
|
return value == 1
|
||||||
|
? NSLocalizedString("1 week", comment: "time interval")
|
||||||
|
: String.localizedStringWithFormat(NSLocalizedString("%d weeks", comment: "time interval"), value)
|
||||||
|
case .month:
|
||||||
|
return value == 1
|
||||||
|
? NSLocalizedString("1 month", comment: "time interval")
|
||||||
|
: String.localizedStringWithFormat(NSLocalizedString("%d months", comment: "time interval"), value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func toShortText(seconds: Int) -> LocalizedStringKey {
|
||||||
|
let (unit, value) = toTimeUnit(seconds: seconds)
|
||||||
|
switch unit {
|
||||||
|
case .second: return "\(value)s"
|
||||||
|
case .minute: return "\(value)m"
|
||||||
|
case .hour: return "\(value)h"
|
||||||
|
case .day: return "\(value)d"
|
||||||
|
case .week: return "\(value)w"
|
||||||
|
case .month: return "\(value)mth"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public func timeText(_ seconds: Int?) -> String {
|
||||||
|
guard let seconds = seconds else { return "off" }
|
||||||
|
if seconds == 0 { return "0 sec" }
|
||||||
|
return CustomTimeUnit.toText(seconds: seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func shortTimeText(_ seconds: Int?) -> LocalizedStringKey {
|
||||||
|
guard let seconds = seconds else { return "off" }
|
||||||
|
if seconds == 0 { return "0s" }
|
||||||
|
return CustomTimeUnit.toShortText(seconds: seconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct ContactUserPreferences: Decodable {
|
public struct ContactUserPreferences: Decodable {
|
||||||
|
@ -2249,13 +2296,13 @@ public enum CIContent: Decodable, ItemContent {
|
||||||
|
|
||||||
static func featureText(_ feature: Feature, _ enabled: String, _ param: Int?) -> String {
|
static func featureText(_ feature: Feature, _ enabled: String, _ param: Int?) -> String {
|
||||||
feature.hasParam
|
feature.hasParam
|
||||||
? "\(feature.text): \(TimedMessagesPreference.ttlText(param))"
|
? "\(feature.text): \(timeText(param))"
|
||||||
: "\(feature.text): \(enabled)"
|
: "\(feature.text): \(enabled)"
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func preferenceText(_ feature: Feature, _ allowed: FeatureAllowed, _ param: Int?) -> String {
|
public static func preferenceText(_ feature: Feature, _ allowed: FeatureAllowed, _ param: Int?) -> String {
|
||||||
allowed != .no && feature.hasParam && param != nil
|
allowed != .no && feature.hasParam && param != nil
|
||||||
? String.localizedStringWithFormat(NSLocalizedString("offered %@: %@", comment: "feature offered item"), feature.text, TimedMessagesPreference.ttlText(param))
|
? String.localizedStringWithFormat(NSLocalizedString("offered %@: %@", comment: "feature offered item"), feature.text, timeText(param))
|
||||||
: allowed != .no
|
: allowed != .no
|
||||||
? String.localizedStringWithFormat(NSLocalizedString("offered %@", comment: "feature offered item"), feature.text)
|
? String.localizedStringWithFormat(NSLocalizedString("offered %@", comment: "feature offered item"), feature.text)
|
||||||
: String.localizedStringWithFormat(NSLocalizedString("cancelled %@", comment: "feature offered item"), feature.text)
|
: String.localizedStringWithFormat(NSLocalizedString("cancelled %@", comment: "feature offered item"), feature.text)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue