Skip to content

Commit 2409409

Browse files
committed
AyuGram-style message saving: deleted texts + edit history
Port AyuGram Desktop's saveDeletedMessages / saveMessagesHistory / saveForBots into the iOS fork: - Snapshot text before remote deletes and on edits in replayFinalState - File-backed MessageSavingStore (text only, like AyuGram) - Extras toggles + View Deleted / Edit History entry points
1 parent 4053e2a commit 2409409

11 files changed

Lines changed: 691 additions & 4 deletions

File tree

‎docs/SIDELOAD.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ features (stored in AccountManager + App Group UserDefaults for NSE):
7676
| Hide Mention Notifications | Drops mention pushes in Notification Service (`loc-key` / alert heuristics; App Group + standard defaults) |
7777
| Hide Pinned Notifications | Drops pinned-message pushes the same way |
7878
| Keychain Session Backup | Mirrors `AccountBackupData` into the device Keychain for same-bundle E-Sign reinstalls |
79+
| Save Deleted Messages | AyuGram-style: keep text of remotely deleted messages; **View Deleted** in the chat ⋯ menu |
80+
| Save Edit History | Keep previous text when others edit; **Edit History** in the message context menu |
81+
| Also Save Bot Messages | Include bot chats in the deleted/edit snapshot store |
7982

8083
Settings → Support footer shows `Telegram VERSION (BUILD)` and `decoder-dev`. Archive tip sheets / auto-archive suggestion alerts are suppressed.
8184

‎submodules/SettingsUI/Sources/ForkExtrasController.swift‎

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,16 @@ private enum ForkExtrasLocalizedString {
4747
"ForkExtras.BackendDefault": "Default",
4848
"ForkExtras.BackendSystem": "System (Apple)",
4949
"ForkExtras.BackendApple": "On-Device (Apple)",
50+
"ForkExtras.SaveDeletedMessages": "Save Deleted Messages",
51+
"ForkExtras.SaveDeletedMessagesFooter": "Keep text of messages others delete. Open View Deleted from the chat menu.",
52+
"ForkExtras.SaveMessagesHistory": "Save Edit History",
53+
"ForkExtras.SaveMessagesHistoryFooter": "Keep previous text when a message is edited. Open Edit History from the message menu.",
54+
"ForkExtras.SaveForBots": "Also Save Bot Messages",
55+
"ForkExtras.ViewDeleted": "View Deleted",
56+
"ForkExtras.EditHistory": "Edit History",
57+
"ForkExtras.ClearDeleted": "Clear Deleted",
58+
"ForkExtras.NoDeleted": "No deleted messages saved yet.",
59+
"ForkExtras.NoEdits": "No previous versions saved.",
5060
],
5161
"ru": [
5262
"ForkExtras.Title": "Дополнительно",
@@ -84,6 +94,16 @@ private enum ForkExtrasLocalizedString {
8494
"ForkExtras.BackendDefault": "По умолчанию",
8595
"ForkExtras.BackendSystem": "Системный (Apple)",
8696
"ForkExtras.BackendApple": "На устройстве (Apple)",
97+
"ForkExtras.SaveDeletedMessages": "Сохранять удалённые",
98+
"ForkExtras.SaveDeletedMessagesFooter": "Хранить текст сообщений, которые удалили другие. Смотрите «Удалённые» в меню чата.",
99+
"ForkExtras.SaveMessagesHistory": "История правок",
100+
"ForkExtras.SaveMessagesHistoryFooter": "Хранить предыдущий текст при редактировании. Открывается из меню сообщения.",
101+
"ForkExtras.SaveForBots": "Также сохранять ботов",
102+
"ForkExtras.ViewDeleted": "Удалённые",
103+
"ForkExtras.EditHistory": "История правок",
104+
"ForkExtras.ClearDeleted": "Очистить удалённые",
105+
"ForkExtras.NoDeleted": "Пока нет сохранённых удалённых сообщений.",
106+
"ForkExtras.NoEdits": "Предыдущих версий нет.",
87107
],
88108
]
89109

@@ -138,6 +158,16 @@ private enum ForkExtrasLocalizedString {
138158
static var translationFooter: String { string(forKey: "ForkExtras.TranslationFooter") }
139159
static var scrollToNextChat: String { string(forKey: "ForkExtras.ScrollToNextChat") }
140160
static var scrollToNextChatFooter: String { string(forKey: "ForkExtras.ScrollToNextChatFooter") }
161+
static var saveDeletedMessages: String { string(forKey: "ForkExtras.SaveDeletedMessages") }
162+
static var saveDeletedMessagesFooter: String { string(forKey: "ForkExtras.SaveDeletedMessagesFooter") }
163+
static var saveMessagesHistory: String { string(forKey: "ForkExtras.SaveMessagesHistory") }
164+
static var saveMessagesHistoryFooter: String { string(forKey: "ForkExtras.SaveMessagesHistoryFooter") }
165+
static var saveForBots: String { string(forKey: "ForkExtras.SaveForBots") }
166+
static var viewDeleted: String { string(forKey: "ForkExtras.ViewDeleted") }
167+
static var editHistory: String { string(forKey: "ForkExtras.EditHistory") }
168+
static var clearDeleted: String { string(forKey: "ForkExtras.ClearDeleted") }
169+
static var noDeleted: String { string(forKey: "ForkExtras.NoDeleted") }
170+
static var noEdits: String { string(forKey: "ForkExtras.NoEdits") }
141171
static var backendDefault: String { string(forKey: "ForkExtras.BackendDefault") }
142172
static var backendSystem: String { string(forKey: "ForkExtras.BackendSystem") }
143173
static var backendApple: String { string(forKey: "ForkExtras.BackendApple") }
@@ -162,6 +192,9 @@ private final class ForkExtrasControllerArguments {
162192
let openTranslationBackend: () -> Void
163193
let openTranscriptionBackend: () -> Void
164194
let updateScrollToNextChatDisabled: (Bool) -> Void
195+
let updateSaveDeletedMessages: (Bool) -> Void
196+
let updateSaveMessagesHistory: (Bool) -> Void
197+
let updateSaveForBots: (Bool) -> Void
165198

166199
init(
167200
updateGhostMode: @escaping (Bool) -> Void,
@@ -181,7 +214,10 @@ private final class ForkExtrasControllerArguments {
181214
updateForceBuiltInMic: @escaping (Bool) -> Void,
182215
openTranslationBackend: @escaping () -> Void,
183216
openTranscriptionBackend: @escaping () -> Void,
184-
updateScrollToNextChatDisabled: @escaping (Bool) -> Void
217+
updateScrollToNextChatDisabled: @escaping (Bool) -> Void,
218+
updateSaveDeletedMessages: @escaping (Bool) -> Void,
219+
updateSaveMessagesHistory: @escaping (Bool) -> Void,
220+
updateSaveForBots: @escaping (Bool) -> Void
185221
) {
186222
self.updateGhostMode = updateGhostMode
187223
self.updateInstantPasscode = updateInstantPasscode
@@ -201,6 +237,9 @@ private final class ForkExtrasControllerArguments {
201237
self.openTranslationBackend = openTranslationBackend
202238
self.openTranscriptionBackend = openTranscriptionBackend
203239
self.updateScrollToNextChatDisabled = updateScrollToNextChatDisabled
240+
self.updateSaveDeletedMessages = updateSaveDeletedMessages
241+
self.updateSaveMessagesHistory = updateSaveMessagesHistory
242+
self.updateSaveForBots = updateSaveForBots
204243
}
205244
}
206245

@@ -214,6 +253,7 @@ private enum ForkExtrasSection: Int32 {
214253
case calls
215254
case translation
216255
case navigation
256+
case messageSaving
217257
}
218258

219259
private enum ForkExtrasEntry: ItemListNodeEntry {
@@ -246,6 +286,11 @@ private enum ForkExtrasEntry: ItemListNodeEntry {
246286
case translationFooter
247287
case scrollToNextChat(Bool)
248288
case scrollToNextChatFooter
289+
case saveDeletedMessages(Bool)
290+
case saveDeletedMessagesFooter
291+
case saveMessagesHistory(Bool)
292+
case saveMessagesHistoryFooter
293+
case saveForBots(Bool)
249294

250295
var section: ItemListSectionId {
251296
switch self {
@@ -267,6 +312,8 @@ private enum ForkExtrasEntry: ItemListNodeEntry {
267312
return ForkExtrasSection.translation.rawValue
268313
case .scrollToNextChat, .scrollToNextChatFooter:
269314
return ForkExtrasSection.navigation.rawValue
315+
case .saveDeletedMessages, .saveDeletedMessagesFooter, .saveMessagesHistory, .saveMessagesHistoryFooter, .saveForBots:
316+
return ForkExtrasSection.messageSaving.rawValue
270317
}
271318
}
272319

@@ -301,6 +348,11 @@ private enum ForkExtrasEntry: ItemListNodeEntry {
301348
case .translationFooter: return 26
302349
case .scrollToNextChat: return 27
303350
case .scrollToNextChatFooter: return 28
351+
case .saveDeletedMessages: return 29
352+
case .saveDeletedMessagesFooter: return 30
353+
case .saveMessagesHistory: return 31
354+
case .saveMessagesHistoryFooter: return 32
355+
case .saveForBots: return 33
304356
}
305357
}
306358

@@ -419,6 +471,22 @@ private enum ForkExtrasEntry: ItemListNodeEntry {
419471
})
420472
case .scrollToNextChatFooter:
421473
return ItemListTextItem(presentationData: presentationData, text: .plain(ForkExtrasLocalizedString.scrollToNextChatFooter), sectionId: self.section)
474+
case let .saveDeletedMessages(value):
475+
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: ForkExtrasLocalizedString.saveDeletedMessages, value: value, sectionId: self.section, style: .blocks, updated: { value in
476+
arguments.updateSaveDeletedMessages(value)
477+
})
478+
case .saveDeletedMessagesFooter:
479+
return ItemListTextItem(presentationData: presentationData, text: .plain(ForkExtrasLocalizedString.saveDeletedMessagesFooter), sectionId: self.section)
480+
case let .saveMessagesHistory(value):
481+
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: ForkExtrasLocalizedString.saveMessagesHistory, value: value, sectionId: self.section, style: .blocks, updated: { value in
482+
arguments.updateSaveMessagesHistory(value)
483+
})
484+
case .saveMessagesHistoryFooter:
485+
return ItemListTextItem(presentationData: presentationData, text: .plain(ForkExtrasLocalizedString.saveMessagesHistoryFooter), sectionId: self.section)
486+
case let .saveForBots(value):
487+
return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: ForkExtrasLocalizedString.saveForBots, value: value, sectionId: self.section, style: .blocks, updated: { value in
488+
arguments.updateSaveForBots(value)
489+
})
422490
}
423491
}
424492
}
@@ -454,6 +522,11 @@ private func forkExtrasControllerEntries(settings: ForkExtrasSettings) -> [ForkE
454522
.translationFooter,
455523
.scrollToNextChat(settings.scrollToNextChatDisabled),
456524
.scrollToNextChatFooter,
525+
.saveDeletedMessages(settings.saveDeletedMessages),
526+
.saveDeletedMessagesFooter,
527+
.saveMessagesHistory(settings.saveMessagesHistory),
528+
.saveMessagesHistoryFooter,
529+
.saveForBots(settings.saveForBots),
457530
]
458531
}
459532

@@ -633,6 +706,27 @@ public func forkExtrasController(context: AccountContext) -> ViewController {
633706
updated.scrollToNextChatDisabled = value
634707
return updated
635708
}.start())
709+
},
710+
updateSaveDeletedMessages: { value in
711+
updateDisposable.set(updateForkExtrasSettingsInteractively(accountManager: context.sharedContext.accountManager) { current in
712+
var updated = current
713+
updated.saveDeletedMessages = value
714+
return updated
715+
}.start())
716+
},
717+
updateSaveMessagesHistory: { value in
718+
updateDisposable.set(updateForkExtrasSettingsInteractively(accountManager: context.sharedContext.accountManager) { current in
719+
var updated = current
720+
updated.saveMessagesHistory = value
721+
return updated
722+
}.start())
723+
},
724+
updateSaveForBots: { value in
725+
updateDisposable.set(updateForkExtrasSettingsInteractively(accountManager: context.sharedContext.accountManager) { current in
726+
var updated = current
727+
updated.saveForBots = value
728+
return updated
729+
}.start())
636730
}
637731
)
638732

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import Foundation
2+
import UIKit
3+
import Display
4+
import SwiftSignalKit
5+
import TelegramCore
6+
import TelegramPresentationData
7+
import TelegramUIPreferences
8+
import ItemListUI
9+
import PresentationDataUtils
10+
import AccountContext
11+
12+
private enum MessageSavingHistoryMode {
13+
case deleted(peerId: EnginePeer.Id, topicId: Int64?)
14+
case edits(peerId: EnginePeer.Id, messageId: EngineMessage.Id)
15+
}
16+
17+
private enum MessageSavingHistoryEntry: ItemListNodeEntry {
18+
case empty(String)
19+
case record(Int, MessageSavingRecord)
20+
21+
var section: ItemListSectionId { 0 }
22+
23+
var stableId: Int32 {
24+
switch self {
25+
case .empty:
26+
return -1
27+
case let .record(index, _):
28+
return Int32(index)
29+
}
30+
}
31+
32+
static func <(lhs: MessageSavingHistoryEntry, rhs: MessageSavingHistoryEntry) -> Bool {
33+
return lhs.stableId < rhs.stableId
34+
}
35+
36+
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
37+
switch self {
38+
case let .empty(text):
39+
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
40+
case let .record(_, record):
41+
let date = Date(timeIntervalSince1970: TimeInterval(record.date))
42+
let formatter = DateFormatter()
43+
formatter.dateStyle = .medium
44+
formatter.timeStyle = .short
45+
let header = "\(record.authorName) · \(formatter.string(from: date))"
46+
return ItemListMultilineTextItem(
47+
presentationData: presentationData,
48+
text: "\(header)\n\(record.text)",
49+
enabledEntityTypes: [],
50+
sectionId: self.section,
51+
style: .blocks
52+
)
53+
}
54+
}
55+
}
56+
57+
private func messageSavingHistoryEntries(mode: MessageSavingHistoryMode, accountPeerId: EnginePeer.Id, emptyText: String) -> [MessageSavingHistoryEntry] {
58+
let accountId = accountPeerId.toInt64()
59+
let records: [MessageSavingRecord]
60+
switch mode {
61+
case let .deleted(peerId, topicId):
62+
records = MessageSavingStore.deleted(accountPeerId: accountId, peerId: peerId.toInt64(), topicId: topicId)
63+
case let .edits(peerId, messageId):
64+
records = MessageSavingStore.edits(
65+
accountPeerId: accountId,
66+
peerId: peerId.toInt64(),
67+
messageId: messageId.id,
68+
namespace: messageId.namespace
69+
)
70+
}
71+
if records.isEmpty {
72+
return [.empty(emptyText)]
73+
}
74+
return records.enumerated().map { .record($0.offset, $0.element) }
75+
}
76+
77+
/// AyuGram-style "View Deleted" list for a peer.
78+
public func messageSavingDeletedController(context: AccountContext, peerId: EnginePeer.Id, topicId: Int64? = nil) -> ViewController {
79+
return messageSavingHistoryController(
80+
context: context,
81+
title: messageSavingLocalizedString(key: "ForkExtras.ViewDeleted", en: "View Deleted", ru: "Удалённые"),
82+
emptyText: messageSavingLocalizedString(key: "ForkExtras.NoDeleted", en: "No deleted messages saved yet.", ru: "Пока нет сохранённых удалённых сообщений."),
83+
mode: .deleted(peerId: peerId, topicId: topicId),
84+
clearAction: {
85+
MessageSavingStore.clearDeleted(
86+
accountPeerId: context.account.peerId.toInt64(),
87+
peerId: peerId.toInt64(),
88+
topicId: topicId
89+
)
90+
}
91+
)
92+
}
93+
94+
/// AyuGram-style edit history for one message.
95+
public func messageSavingEditsController(context: AccountContext, messageId: EngineMessage.Id) -> ViewController {
96+
return messageSavingHistoryController(
97+
context: context,
98+
title: messageSavingLocalizedString(key: "ForkExtras.EditHistory", en: "Edit History", ru: "История правок"),
99+
emptyText: messageSavingLocalizedString(key: "ForkExtras.NoEdits", en: "No previous versions saved.", ru: "Предыдущих версий нет."),
100+
mode: .edits(peerId: messageId.peerId, messageId: messageId),
101+
clearAction: nil
102+
)
103+
}
104+
105+
private func messageSavingHistoryController(
106+
context: AccountContext,
107+
title: String,
108+
emptyText: String,
109+
mode: MessageSavingHistoryMode,
110+
clearAction: (() -> Void)?
111+
) -> ViewController {
112+
let refresh = Promise<Void>()
113+
refresh.set(.single(Void()))
114+
115+
let signal = combineLatest(
116+
context.sharedContext.presentationData,
117+
refresh.get()
118+
)
119+
|> deliverOnMainQueue
120+
|> map { presentationData, _ -> (ItemListControllerState, (ItemListNodeState, Any)) in
121+
var rightButton: ItemListNavigationButton?
122+
if clearAction != nil {
123+
rightButton = ItemListNavigationButton(
124+
content: .text(messageSavingLocalizedString(key: "ForkExtras.ClearDeleted", en: "Clear Deleted", ru: "Очистить удалённые")),
125+
style: .regular,
126+
enabled: true,
127+
action: {
128+
clearAction?()
129+
refresh.set(.single(Void()))
130+
}
131+
)
132+
}
133+
let controllerState = ItemListControllerState(
134+
presentationData: ItemListPresentationData(presentationData),
135+
title: .text(title),
136+
leftNavigationButton: nil,
137+
rightNavigationButton: rightButton,
138+
backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)
139+
)
140+
let listState = ItemListNodeState(
141+
presentationData: ItemListPresentationData(presentationData),
142+
entries: messageSavingHistoryEntries(mode: mode, accountPeerId: context.account.peerId, emptyText: emptyText),
143+
style: .blocks
144+
)
145+
return (controllerState, (listState, ()))
146+
}
147+
148+
return ItemListController(context: context, state: signal)
149+
}
150+
151+
private func messageSavingLocalizedString(key: String, en: String, ru: String) -> String {
152+
let candidates = Locale.preferredLanguages + Bundle.main.preferredLocalizations
153+
for candidate in candidates {
154+
let code = String(candidate.prefix(2)).lowercased()
155+
if code == "ru" {
156+
return ru
157+
}
158+
}
159+
return en
160+
}

0 commit comments

Comments
 (0)