Skip to content

Commit 7cc8cc3

Browse files
authored
Allow removing individual emoji from Recent (#199)
* fix(EntertainmentUI): allow removing individual recent emoji * fix(EntertainmentUI): omit empty ordinary recent emoji rows * fix(EntertainmentUI): preserve Recent rows in all emoji pickers
1 parent ac870ab commit 7cc8cc3

10 files changed

Lines changed: 1322 additions & 12 deletions

File tree

‎packages/EntertainmentUI/Sources/EntertainmentUI/EBlockItem.swift‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import TelegramCore
44
import SwiftSignalKit
55
import InAppSettings
66
import Strings
7+
import Localization
8+
import LocalAnimations
9+
10+
let recentEmojiSourceKey = NSAttributedString.Key("recentEmojiSource")
711

812
public class EBlockItem: TableRowItem {
913

@@ -25,12 +29,14 @@ public class EBlockItem: TableRowItem {
2529
public let segment: EmojiSegment
2630
public internal(set) var selectedOrders: [String: Int]
2731
public let selectionColor: NSColor?
32+
public let removeRecent: ((String) -> Void)?
2833

29-
public init(_ initialSize:NSSize, stableId: AnyHashable, attrLines:[[NSAttributedString]], segment: EmojiSegment, account: Account, selectHandler:@escaping(String, NSRect)->Void, dragHandler: ((String, EntertainmentStickerDrag) -> Bool)? = nil, selectedOrders: [String: Int] = [:], selectionColor: NSColor? = nil) {
34+
public init(_ initialSize:NSSize, stableId: AnyHashable, attrLines:[[NSAttributedString]], segment: EmojiSegment, account: Account, selectHandler:@escaping(String, NSRect)->Void, dragHandler: ((String, EntertainmentStickerDrag) -> Bool)? = nil, selectedOrders: [String: Int] = [:], selectionColor: NSColor? = nil, removeRecent: ((String) -> Void)? = nil) {
3035
self.lineAttr = attrLines
3136
self.symbols = attrLines.flatMap { $0 }
3237
self.selectedOrders = selectedOrders
3338
self.selectionColor = selectionColor
39+
self.removeRecent = removeRecent
3440
self._stableId = stableId
3541
self.account = account
3642
self.segment = segment
@@ -66,12 +72,22 @@ public class EBlockItem: TableRowItem {
6672
public override func menuItems(in location: NSPoint) -> Signal<[ContextMenuItem], NoError> {
6773
var items: [ContextMenuItem] = []
6874
let postbox = self.account.postbox
69-
if let view = self.view as? EBlockRowView {
70-
if let emoji = view.emojiUnderMouse?.emojiUnmodified, emoji.canHaveSkinToneModifier {
75+
if let view = self.view as? EBlockRowView, let selectedEmoji = view.emoji(at: location) {
76+
let emoji = selectedEmoji.string.emojiUnmodified
77+
if emoji.canHaveSkinToneModifier {
7178
items.append(EmojiToleranceContextMenuItem(emoji: emoji, callback: { value in
7279
_ = modifySkinEmoji(emoji, modifier: value, postbox: postbox).start()
7380
}))
7481
}
82+
if let removeRecent = removeRecent {
83+
if !items.isEmpty {
84+
items.append(ContextSeparatorItem())
85+
}
86+
let source = selectedEmoji.attribute(recentEmojiSourceKey, at: 0, effectiveRange: nil) as? String ?? selectedEmoji.string
87+
items.append(ContextMenuItem(strings().searchRemoveFromRecent, handler: {
88+
removeRecent(source)
89+
}, itemMode: .destruct, itemImage: MenuAnimation.menu_delete.value))
90+
}
7591
}
7692

7793

‎packages/EntertainmentUI/Sources/EntertainmentUI/EBlockRowView.swift‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,16 @@ public class EBlockRowView: TableRowView {
180180
self.currentDownItem = nil
181181
}
182182

183-
public var emojiUnderMouse: String? {
184-
return itemUnderMouse?.1.string
183+
func emoji(at point: NSPoint) -> NSAttributedString? {
184+
let point = content.convert(point, from: self)
185+
guard content.bounds.contains(point), let item = item as? EBlockItem, let key = lines.first(where: { $0.value.frame.contains(point) })?.key else {
186+
return nil
187+
}
188+
let symbols = item.lineAttr.flatMap { $0 }
189+
guard symbols.indices.contains(key.index) else {
190+
return nil
191+
}
192+
return symbols[key.index]
185193
}
186194

187195
private var itemUnderMouse: (LineLayer, NSAttributedString)? {

‎packages/EntertainmentUI/Sources/EntertainmentUI/EmojiesController.swift‎

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,11 @@ private func segments(_ emoji: [EmojiSegment : [String]], skinModifiers: [EmojiS
180180
let items = list.map { emoji -> NSAttributedString in
181181
let unmodified = emoji.emojiUnmodified
182182
let value = skinModifiers.first(where: { $0.emoji == unmodified }).map { unmodified.emojiWithSkinModifier($0.modifier) } ?? emoji
183-
return .initialize(string: value.normalizedEmoji, font: .normal(26.0))
183+
let attributed = NSMutableAttributedString(attributedString: .initialize(string: value.normalizedEmoji, font: .normal(26.0)))
184+
if key == .Recent {
185+
attributed.addAttribute(recentEmojiSourceKey, value: emoji, range: NSRange(location: 0, length: attributed.length))
186+
}
187+
return attributed
184188
}
185189
let lines = stride(from: 0, to: items.count, by: 8).map {
186190
Array(items[$0 ..< min($0 + 8, items.count)])
@@ -517,9 +521,12 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] {
517521
}
518522
}
519523
let seg = segments(e, skinModifiers: state.recent.skinModifiers)
520-
let seglist = seg.map { (key,_) -> EmojiSegment in
524+
var seglist = seg.map { (key,_) -> EmojiSegment in
521525
return key
522526
}.sorted(by: <)
527+
if !seglist.contains(.Recent) {
528+
seglist.insert(.Recent, at: 0)
529+
}
523530

524531

525532
let isPremium = state.peer?.peer.isPremium == true || arguments.ignorePremium
@@ -814,13 +821,25 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] {
814821
let hasAnimatedRecent = arguments.mode == .emoji || arguments.mode == .stories || arguments.mode == .forumTopic || arguments.mode == .selectAvatar || arguments.mode == .channelReactions
815822

816823
if key == .Recent, !recentAnimated.isEmpty, hasAnimatedRecent {
824+
let removeRecent: ((MediaId) -> Void)?
825+
if arguments.mode == .emoji || arguments.mode == .stories {
826+
removeRecent = { id in
827+
_ = removeAnimatedUsedEmoji(id, postbox: arguments.context.account.postbox).startStandalone()
828+
}
829+
} else {
830+
removeRecent = nil
831+
}
817832
let tuple = Tuple(items: recentAnimated, selected: emojiesRowSelection(state.selectedItems, in: recentAnimated), color: state.color)
818833
entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_emoji_block(EmojiSegment.RecentAnimated.rawValue), equatable: InputDataEquatable(tuple), comparable: nil, item: { initialSize, stableId in
819-
return EmojiesSectionRowItem(initialSize, stableId: stableId, context: arguments.context, revealed: true, installed: true, info: nil, items: recentAnimated, mode: arguments.mode.itemMode, selectedItems: tuple.selected, color: tuple.color, callback: arguments.send, largeCallback: arguments.sendLarge, drag: arguments.drag, ignorePremium: arguments.ignorePremium)
834+
return EmojiesSectionRowItem(initialSize, stableId: stableId, context: arguments.context, revealed: true, installed: true, info: nil, items: recentAnimated, mode: arguments.mode.itemMode, selectedItems: tuple.selected, color: tuple.color, callback: arguments.send, largeCallback: arguments.sendLarge, drag: arguments.drag, ignorePremium: arguments.ignorePremium, removeRecent: removeRecent)
820835
}))
821836
index += 1
822837
}
823838

839+
guard let lines = seg[key], !lines.isEmpty else {
840+
continue
841+
}
842+
824843
if key != .Recent || !recentAnimated.isEmpty, arguments.mode == .emoji || arguments.mode == .stories {
825844
entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_emoji_segment(key.rawValue), equatable: InputDataEquatable(key), comparable: nil, item: { initialSize, stableId in
826845
return EStickItem(initialSize, stableId: stableId, segmentName:key.localizedString)
@@ -836,7 +855,6 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] {
836855
let selectedOrders: [String: Int]
837856
let selectionColor: NSColor?
838857
}
839-
let lines = seg[key]!
840858
var selectedOrders: [String: Int] = [:]
841859
if state.selectedItems.contains(where: { $0.index != nil }) {
842860
let segmentKeys = Set(lines.flatMap { $0.map { emojiSelectionKey($0.string) } })
@@ -849,9 +867,17 @@ private func entries(_ state: State, arguments: Arguments) -> [InputDataEntry] {
849867
}
850868
}
851869
}
870+
let removeRecent: ((String) -> Void)?
871+
if key == .Recent {
872+
removeRecent = { emoji in
873+
_ = removeUsedEmoji(emoji, postbox: arguments.context.account.postbox).startStandalone()
874+
}
875+
} else {
876+
removeRecent = nil
877+
}
852878
let tuple = Tuple(key: key, lines: lines, selectedOrders: selectedOrders, selectionColor: state.color ?? theme.colors.text)
853879
entries.append(.custom(sectionId: sectionId, index: index, value: .none, identifier: _id_emoji_block(key.rawValue), equatable: InputDataEquatable(tuple), comparable: nil, item: { initialSize, stableId in
854-
return EBlockItem(initialSize, stableId: stableId, attrLines: tuple.lines, segment: tuple.key, account: arguments.context.account, selectHandler: arguments.sendEmoji, dragHandler: arguments.dragEmoji, selectedOrders: tuple.selectedOrders, selectionColor: tuple.selectionColor)
880+
return EBlockItem(initialSize, stableId: stableId, attrLines: tuple.lines, segment: tuple.key, account: arguments.context.account, selectHandler: arguments.sendEmoji, dragHandler: arguments.dragEmoji, selectedOrders: tuple.selectedOrders, selectionColor: tuple.selectionColor, removeRecent: removeRecent)
855881
}))
856882
index += 1
857883
}

‎packages/EntertainmentUI/Sources/EntertainmentUI/EmojiesSectionRowItem.swift‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,13 +147,15 @@ public final class EmojiesSectionRowItem : GeneralRowItem {
147147
public let mode: Mode
148148
public let color: NSColor?
149149
public let uniqueGifts: [StarGift.UniqueGift]
150+
public let removeRecent: ((MediaId) -> Void)?
150151
private let emojiStatusAction = EmojiStatusMenuAction()
151-
public init(_ initialSize: NSSize, stableId: AnyHashable, context: any AccountContextProtocol, revealed: Bool, installed: Bool, info: StickerPackCollectionInfo?, items: [StickerPackItem], isBig: Bool = false, groupEmojiPack: Bool = false, mode: Mode = .panel, selectedItems:[SelectedItem] = [], color: NSColor? = nil, callback:@escaping(StickerPackItem, StickerPackCollectionInfo?, Int32?, NSRect?)->Void, largeCallback: (() -> ((StickerPackItem, NSRect?) -> Void)?)? = nil, drag: ((StickerPackItem, EntertainmentStickerDrag) -> Bool)? = nil, viewSet:((StickerPackCollectionInfo)->Void)? = nil, showAllItems:(()->Void)? = nil, openPremium:(()->Void)? = nil, installPack:((StickerPackCollectionInfo, [StickerPackItem])->Void)? = nil, ignorePremium: Bool = false, uniqueGifts: [StarGift.UniqueGift] = []) {
152+
public init(_ initialSize: NSSize, stableId: AnyHashable, context: any AccountContextProtocol, revealed: Bool, installed: Bool, info: StickerPackCollectionInfo?, items: [StickerPackItem], isBig: Bool = false, groupEmojiPack: Bool = false, mode: Mode = .panel, selectedItems:[SelectedItem] = [], color: NSColor? = nil, callback:@escaping(StickerPackItem, StickerPackCollectionInfo?, Int32?, NSRect?)->Void, largeCallback: (() -> ((StickerPackItem, NSRect?) -> Void)?)? = nil, drag: ((StickerPackItem, EntertainmentStickerDrag) -> Bool)? = nil, viewSet:((StickerPackCollectionInfo)->Void)? = nil, showAllItems:(()->Void)? = nil, openPremium:(()->Void)? = nil, installPack:((StickerPackCollectionInfo, [StickerPackItem])->Void)? = nil, ignorePremium: Bool = false, uniqueGifts: [StarGift.UniqueGift] = [], removeRecent: ((MediaId) -> Void)? = nil) {
152153
self.itemSize = isBig ? NSMakeSize(66, 60) : NSMakeSize(40, 34)
153154
self.info = info
154155
self.mode = mode
155156
self.color = color
156157
self.uniqueGifts = uniqueGifts
158+
self.removeRecent = removeRecent
157159
self._items = items
158160
self.viewSet = viewSet
159161
self.installed = installed
@@ -450,6 +452,12 @@ public final class EmojiesSectionRowItem : GeneralRowItem {
450452
items.append(setStatus)
451453
}
452454

455+
if let removeRecent = removeRecent, let view = self.view as? EmojiesSectionRowView, let file = view.item(at: location)?.file {
456+
items.append(ContextMenuItem(strings().searchRemoveFromRecent, handler: {
457+
removeRecent(file.fileId)
458+
}, itemMode: .destruct, itemImage: MenuAnimation.menu_delete.value))
459+
}
460+
453461
if stableId == AnyHashable(0) || self.viewSet == nil {
454462
return .single(items)
455463
}
@@ -787,6 +795,14 @@ private final class EmojiesSectionRowView : TableRowView, ModalPreviewRowViewPro
787795
}
788796
}
789797

798+
fileprivate func item(at point: NSPoint) -> StickerPackItem? {
799+
let point = contentView.convert(point, from: self)
800+
guard contentView.bounds.contains(point), let item = self.item as? EmojiesSectionRowItem else {
801+
return nil
802+
}
803+
return item.items.first(where: { $0.rect.contains(point) })?.item
804+
}
805+
790806
fileprivate var itemUnderMouse: (InlineStickerItemLayer, EmojiesSectionRowItem.Item)? {
791807
guard let window = self.window, let item = self.item as? EmojiesSectionRowItem else {
792808
return nil

‎packages/InAppSettings/Sources/InAppSettings/RecentUsedEmoji.swift‎

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,22 @@ public class RecentUsedEmoji: Codable, Equatable {
121121
}
122122

123123

124+
public func removingEmoji(_ emoji: String) -> RecentUsedEmoji {
125+
let key = emoji.strippedEmoji
126+
let emojies = _emojies.filter { value in
127+
if value.strippedEmoji == key {
128+
return false
129+
}
130+
let displayed = RecentUsedEmoji(emojies: [value], animated: [], skinModifiers: skinModifiers).emojies
131+
return !displayed.contains(where: { $0.strippedEmoji == key })
132+
}
133+
return RecentUsedEmoji(emojies: emojies, animated: animated, skinModifiers: skinModifiers)
134+
}
135+
136+
public func removingAnimatedEmoji(_ id: MediaId) -> RecentUsedEmoji {
137+
return RecentUsedEmoji(emojies: _emojies, animated: animated.filter { $0 != id }, skinModifiers: skinModifiers)
138+
}
139+
124140
public func encode(to encoder: Encoder) throws {
125141
var container = encoder.container(keyedBy: StringCodingKey.self)
126142
try container.encode(_emojies, forKey: "e")
@@ -132,7 +148,7 @@ public class RecentUsedEmoji: Codable, Equatable {
132148
}
133149

134150
public func ==(lhs: RecentUsedEmoji, rhs: RecentUsedEmoji) -> Bool {
135-
return lhs.emojies == rhs.emojies && lhs.skinModifiers == rhs.skinModifiers
151+
return lhs.emojies == rhs.emojies && lhs.skinModifiers == rhs.skinModifiers && lhs.animated == rhs.animated
136152
}
137153

138154

@@ -201,6 +217,24 @@ public func modifySkinEmoji(_ emoji:String, modifier: String?, postbox: Postbox)
201217
}
202218
}
203219

220+
public func removeUsedEmoji(_ emoji: String, postbox: Postbox) -> Signal<Void, NoError> {
221+
return postbox.transaction { transaction -> Void in
222+
transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.recentEmoji, { entry in
223+
let value = entry?.get(RecentUsedEmoji.self) ?? RecentUsedEmoji.defaultSettings
224+
return PreferencesEntry(value.removingEmoji(emoji))
225+
})
226+
}
227+
}
228+
229+
public func removeAnimatedUsedEmoji(_ id: MediaId, postbox: Postbox) -> Signal<Void, NoError> {
230+
return postbox.transaction { transaction -> Void in
231+
transaction.updatePreferencesEntry(key: ApplicationSpecificPreferencesKeys.recentEmoji, { entry in
232+
let value = entry?.get(RecentUsedEmoji.self) ?? RecentUsedEmoji.defaultSettings
233+
return PreferencesEntry(value.removingAnimatedEmoji(id))
234+
})
235+
}
236+
}
237+
204238
public func recentUsedEmoji(postbox: Postbox) -> Signal<RecentUsedEmoji, NoError> {
205239
return postbox.preferencesView(keys: [ApplicationSpecificPreferencesKeys.recentEmoji]) |> map { preferences in
206240
return preferences.values[ApplicationSpecificPreferencesKeys.recentEmoji]?.get(RecentUsedEmoji.self) ?? RecentUsedEmoji.defaultSettings

0 commit comments

Comments
 (0)