damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit b771e8f49a21056799b82511ce907b85205c9f9d
parent a88e80a34618ba27c06f21fb3218e137246464fb
Author: Daniel D’Aquino <daniel@daquino.me>
Date:   Mon, 24 Jun 2024 11:38:31 -0700

Merge pull request #2295 from tyiu/change-emoji-component

Revamp emoji picker to be less error-prone and add search, frequently used, and multiple skin tone support capabilities
Diffstat:
Mdamus.xcodeproj/project.pbxproj | 20++++++++++----------
Mdamus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 41++++++++++++++++++++++++++++++++++-------
Mdamus/ContentView.swift | 4+++-
Mdamus/Models/DamusState.swift | 8++++++--
Mdamus/TestData.swift | 4+++-
Mdamus/Util/Router.swift | 2+-
Mdamus/Views/ActionBar/EventActionBar.swift | 39+++++++++++++--------------------------
Mdamus/Views/Chat/ChatEventView.swift | 30+++++++++++++++---------------
Mdamus/Views/Settings/ReactionsSettingsView.swift | 58++++++++++++++++++----------------------------------------
MdamusTests/Mocking/MockDamusState.swift | 5++++-
10 files changed, 107 insertions(+), 104 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */; }; 3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAEC294FCCFC00EE4006 /* Constants.swift */; }; 31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D2E846295218AF006D67F8 /* Shimmer.swift */; }; + 3A0A30BB2C21397A00F8C9BC /* EmojiPicker in Frameworks */ = {isa = PBXBuildFile; productRef = 3A0A30BA2C21397A00F8C9BC /* EmojiPicker */; }; 3A23838E2A297DD200E5AA2E /* ZapButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */; }; 3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */; }; 3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */; }; @@ -32,7 +33,6 @@ 3ACB685F297633BC00C46468 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685D297633BC00C46468 /* Localizable.strings */; }; 3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */; }; 3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */; }; - 3AFE89C32BD4156F00AD31EF /* MCEmojiPicker in Frameworks */ = {isa = PBXBuildFile; productRef = 3AFE89C22BD4156F00AD31EF /* MCEmojiPicker */; }; 3CCD1E6A2A874C4E0099A953 /* Nip98HTTPAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCD1E692A874C4E0099A953 /* Nip98HTTPAuth.swift */; }; 4C011B5E2BD0A56A002F2F9B /* ChatEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C011B5C2BD0A56A002F2F9B /* ChatEventView.swift */; }; 4C011B5F2BD0A56A002F2F9B /* ChatroomThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C011B5D2BD0A56A002F2F9B /* ChatroomThreadView.swift */; }; @@ -1497,10 +1497,10 @@ buildActionMask = 2147483647; files = ( 4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */, + 3A0A30BB2C21397A00F8C9BC /* EmojiPicker in Frameworks */, D78DB8592C1CE9CA00F0AB12 /* SwipeActions in Frameworks */, 4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */, 4C27C9322A64766F007DBC75 /* MarkdownUI in Frameworks */, - 3AFE89C32BD4156F00AD31EF /* MCEmojiPicker in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2893,7 +2893,7 @@ 4C649880286E0EE300EAE2B3 /* secp256k1 */, 4C06670328FC7EC500038D2A /* Kingfisher */, 4C27C9312A64766F007DBC75 /* MarkdownUI */, - 3AFE89C22BD4156F00AD31EF /* MCEmojiPicker */, + 3A0A30BA2C21397A00F8C9BC /* EmojiPicker */, D78DB8582C1CE9CA00F0AB12 /* SwipeActions */, ); productName = damus; @@ -3034,7 +3034,7 @@ 4CCF9AB02A1FE80B00E03CFB /* XCRemoteSwiftPackageReference "GSPlayer" */, 4C27C9302A64766F007DBC75 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */, D7A343EC2AD0D77C00CED48B /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */, - 3AFE89C12BD4156F00AD31EF /* XCRemoteSwiftPackageReference "MCEmojiPicker" */, + 3A0A30B92C21397A00F8C9BC /* XCRemoteSwiftPackageReference "EmojiPicker" */, D78DB8572C1CE9CA00F0AB12 /* XCRemoteSwiftPackageReference "SwipeActions" */, ); productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */; @@ -4349,12 +4349,12 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 3AFE89C12BD4156F00AD31EF /* XCRemoteSwiftPackageReference "MCEmojiPicker" */ = { + 3A0A30B92C21397A00F8C9BC /* XCRemoteSwiftPackageReference "EmojiPicker" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/izyumkin/MCEmojiPicker"; + repositoryURL = "https://github.com/tyiu/EmojiPicker.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.2.3; + minimumVersion = 0.1.1; }; }; 4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */ = { @@ -4408,10 +4408,10 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 3AFE89C22BD4156F00AD31EF /* MCEmojiPicker */ = { + 3A0A30BA2C21397A00F8C9BC /* EmojiPicker */ = { isa = XCSwiftPackageProductDependency; - package = 3AFE89C12BD4156F00AD31EF /* XCRemoteSwiftPackageReference "MCEmojiPicker" */; - productName = MCEmojiPicker; + package = 3A0A30B92C21397A00F8C9BC /* XCRemoteSwiftPackageReference "EmojiPicker" */; + productName = EmojiPicker; }; 4C06670328FC7EC500038D2A /* Kingfisher */ = { isa = XCSwiftPackageProductDependency; diff --git a/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -2,6 +2,24 @@ "originHash" : "babaf4d5748afecf49bbb702530d8e9576460692f478b0a50ee43195dd4440e2", "pins" : [ { + "identity" : "emojikit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tyiu/EmojiKit", + "state" : { + "revision" : "05805f72d63a6d6a2d7dc7fe14abd37c1317b11a", + "version" : "0.1.2" + } + }, + { + "identity" : "emojipicker", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tyiu/EmojiPicker.git", + "state" : { + "revision" : "0c28b4a1a6b8840cf2580bda59517f6d0a733dc8", + "version" : "0.1.1" + } + }, + { "identity" : "gsplayer", "kind" : "remoteSourceControl", "location" : "https://github.com/wxxsw/GSPlayer", @@ -20,20 +38,20 @@ } }, { - "identity" : "mcemojipicker", + "identity" : "secp256k1.swift", "kind" : "remoteSourceControl", - "location" : "https://github.com/izyumkin/MCEmojiPicker", + "location" : "https://github.com/jb55/secp256k1.swift", "state" : { - "revision" : "e0b4903b75ae1cc418d276d84d1cb946b8a1d73c", - "version" : "1.2.3" + "revision" : "40b4b38b3b1c83f7088c76189a742870e0ca06a9" } }, { - "identity" : "secp256k1.swift", + "identity" : "swift-collections", "kind" : "remoteSourceControl", - "location" : "https://github.com/jb55/secp256k1.swift", + "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "40b4b38b3b1c83f7088c76189a742870e0ca06a9" + "revision" : "ee97538f5b81ae89698fd95938896dec5217b148", + "version" : "1.1.1" } }, { @@ -63,6 +81,15 @@ } }, { + "identity" : "swift-trie", + "kind" : "remoteSourceControl", + "location" : "https://github.com/tyiu/swift-trie", + "state" : { + "revision" : "4c50bff6c168f74425f70476be62a072980d2da7", + "version" : "0.1.2" + } + }, + { "identity" : "swipeactions", "kind" : "remoteSourceControl", "location" : "https://github.com/aheze/SwipeActions", diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -8,6 +8,7 @@ import SwiftUI import AVKit import MediaPlayer +import EmojiPicker struct ZapSheet { let target: ZapTarget @@ -719,7 +720,8 @@ struct ContentView: View { music: MusicController(onChange: music_changed), video: VideoController(), ndb: ndb, - quote_reposts: .init(our_pubkey: pubkey) + quote_reposts: .init(our_pubkey: pubkey), + emoji_provider: DefaultEmojiProvider(showAllVariations: true) ) home.damus_state = self.damus_state! diff --git a/damus/Models/DamusState.swift b/damus/Models/DamusState.swift @@ -7,6 +7,7 @@ import Foundation import LinkPresentation +import EmojiPicker class DamusState: HeadlessDamusState { let pool: RelayPool @@ -37,8 +38,9 @@ class DamusState: HeadlessDamusState { let ndb: Ndb var purple: DamusPurple var push_notification_client: PushNotificationClient + let emoji_provider: EmojiProvider - init(pool: RelayPool, keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, postbox: PostBox, bootstrap_relays: [RelayURL], replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: VideoController, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter) { + init(pool: RelayPool, keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, postbox: PostBox, bootstrap_relays: [RelayURL], replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: VideoController, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider) { self.pool = pool self.keypair = keypair self.likes = likes @@ -70,6 +72,7 @@ class DamusState: HeadlessDamusState { ) self.quote_reposts = quote_reposts self.push_notification_client = PushNotificationClient(keypair: keypair, settings: settings) + self.emoji_provider = emoji_provider } @discardableResult @@ -135,7 +138,8 @@ class DamusState: HeadlessDamusState { music: nil, video: VideoController(), ndb: .empty, - quote_reposts: .init(our_pubkey: empty_pub) + quote_reposts: .init(our_pubkey: empty_pub), + emoji_provider: DefaultEmojiProvider(showAllVariations: true) ) } } diff --git a/damus/TestData.swift b/damus/TestData.swift @@ -6,6 +6,7 @@ // import Foundation +import EmojiPicker let test_seckey = Privkey(Data([0xe0, 0xaa, 0x60, 0x26, 0x08, 0x18, 0xac, 0x10, 0x03, 0x86, 0x4d, 0x15, 0x24, 0x9a, 0xf7, 0xa3, 0x3e, 0x4f, 0x1f, 0xc9, 0x01, 0xcf, 0xee, 0xa9, 0xb4, 0x77, 0xc7, 0x07, 0x22, 0xb7, 0x25, 0xfd])) @@ -100,7 +101,8 @@ var test_damus_state: DamusState = ({ music: .init(onChange: {_ in }), video: .init(), ndb: ndb, - quote_reposts: .init(our_pubkey: our_pubkey) + quote_reposts: .init(our_pubkey: our_pubkey), + emoji_provider: DefaultEmojiProvider(showAllVariations: true) ) /* diff --git a/damus/Util/Router.swift b/damus/Util/Router.swift @@ -85,7 +85,7 @@ enum Route: Hashable { case .TranslationSettings(let settings): TranslationSettingsView(settings: settings, damus_state: damusState) case .ReactionsSettings(let settings): - ReactionsSettingsView(settings: settings) + ReactionsSettingsView(settings: settings, damus_state: damusState) case .SearchSettings(let settings): SearchSettingsView(settings: settings) case .DeveloperSettings(let settings): diff --git a/damus/Views/ActionBar/EventActionBar.swift b/damus/Views/ActionBar/EventActionBar.swift @@ -6,7 +6,8 @@ // import SwiftUI -import MCEmojiPicker +import EmojiPicker +import EmojiKit import SwipeActions struct EventActionBar: View { @@ -22,7 +23,7 @@ struct EventActionBar: View { @State var show_share_action: Bool = false @State var show_repost_action: Bool = false - @State private var isOnTopHalfOfScreen: Bool = false + @State private var selectedEmoji: Emoji? = nil @ObservedObject var bar: ActionBarModel @@ -126,7 +127,7 @@ struct EventActionBar: View { var like_button: some View { HStack(spacing: 4) { - LikeButton(damus_state: damus_state, liked: bar.liked, liked_emoji: bar.our_like != nil ? to_reaction_emoji(ev: bar.our_like!) : nil, isOnTopHalfOfScreen: $isOnTopHalfOfScreen) { emoji in + LikeButton(damus_state: damus_state, liked: bar.liked, liked_emoji: bar.our_like != nil ? to_reaction_emoji(ev: bar.our_like!) : nil) { emoji in if bar.liked { //notify(.delete, bar.our_like) } else { @@ -257,20 +258,6 @@ struct EventActionBar: View { self.bar.our_like = liked.event } } - .background( - GeometryReader { geometry in - EmptyView() - .onAppear { - let eventActionBarY = geometry.frame(in: .global).midY - let screenMidY = UIScreen.main.bounds.midY - self.isOnTopHalfOfScreen = eventActionBarY > screenMidY - } - .onChange(of: geometry.frame(in: .global).midY) { newY in - let screenMidY = UIScreen.main.bounds.midY - self.isOnTopHalfOfScreen = newY > screenMidY - } - } - ) } func send_like(emoji: String) { @@ -315,7 +302,6 @@ struct LikeButton: View { let damus_state: DamusState let liked: Bool let liked_emoji: String? - @Binding var isOnTopHalfOfScreen: Bool let action: (_ emoji: String) -> Void // For reactions background @@ -324,7 +310,7 @@ struct LikeButton: View { @State private var isReactionsVisible = false - @State private var selectedEmoji: String = "" + @State private var selectedEmoji: Emoji? // Following four are Shaka animation properties let timer = Timer.publish(every: 0.10, on: .main, in: .common).autoconnect() @@ -363,6 +349,11 @@ struct LikeButton: View { .foregroundColor(.gray) } } + .sheet(isPresented: $isReactionsVisible) { + NavigationView { + EmojiPickerView(selectedEmoji: $selectedEmoji, emojiProvider: damus_state.emoji_provider) + }.presentationDetents([.medium, .large]) + } .accessibilityLabel(NSLocalizedString("Like", comment: "Accessibility Label for Like button")) .rotationEffect(Angle(degrees: shouldAnimate ? rotationAngle : 0)) .onReceive(self.timer) { _ in @@ -377,14 +368,10 @@ struct LikeButton: View { amountOfAngleIncrease = 20.0 } }) - .emojiPicker( - isPresented: $isReactionsVisible, - selectedEmoji: $selectedEmoji, - arrowDirection: isOnTopHalfOfScreen ? .down : .up, - isDismissAfterChoosing: true - ) .onChange(of: selectedEmoji) { newSelectedEmoji in - self.action(newSelectedEmoji) + if let newSelectedEmoji { + self.action(newSelectedEmoji.value) + } } } diff --git a/damus/Views/Chat/ChatEventView.swift b/damus/Views/Chat/ChatEventView.swift @@ -6,7 +6,8 @@ // import SwiftUI -import MCEmojiPicker +import EmojiKit +import EmojiPicker import SwipeActions fileprivate let CORNER_RADIUS: CGFloat = 10 @@ -34,8 +35,8 @@ struct ChatEventView: View { generator.impactOccurred() } } - @State var selected_emoji: String = "" - + @State var selected_emoji: Emoji? + @State private var isOnTopHalfOfScreen: Bool = false @ObservedObject var bar: ActionBarModel @@ -154,19 +155,18 @@ struct ChatEventView: View { var event_bubble_with_long_press_interaction: some View { ZStack(alignment: is_ours ? .bottomLeading : .bottomTrailing) { self.event_bubble - .emojiPicker( - isPresented: Binding(get: { popover_state == .open_emoji_selector }, set: { new_state in - withAnimation(new_state == true ? .easeIn(duration: 0.5) : .easeOut(duration: 0.1)) { - popover_state = new_state == true ? .open_emoji_selector : .closed - } - }), - selectedEmoji: $selected_emoji, - arrowDirection: isOnTopHalfOfScreen ? .down : .up, - isDismissAfterChoosing: false - ) + .sheet(isPresented: Binding(get: { popover_state == .open_emoji_selector }, set: { new_state in + withAnimation(new_state == true ? .easeIn(duration: 0.5) : .easeOut(duration: 0.1)) { + popover_state = new_state == true ? .open_emoji_selector : .closed + } + })) { + NavigationView { + EmojiPickerView(selectedEmoji: $selected_emoji, emojiProvider: damus_state.emoji_provider) + }.presentationDetents([.medium, .large]) + } .onChange(of: selected_emoji) { newSelectedEmoji in - if newSelectedEmoji != "" { - send_like(emoji: newSelectedEmoji) + if let newSelectedEmoji { + send_like(emoji: newSelectedEmoji.value) popover_state = .closed } } diff --git a/damus/Views/Settings/ReactionsSettingsView.swift b/damus/Views/Settings/ReactionsSettingsView.swift @@ -6,22 +6,20 @@ // import SwiftUI -import MCEmojiPicker +import EmojiPicker +import EmojiKit struct ReactionsSettingsView: View { @ObservedObject var settings: UserSettingsStore + let damus_state: DamusState @State private var isReactionsVisible: Bool = false + @State private var selectedEmoji: Emoji? = nil + var body: some View { Form { Section { Text(settings.default_emoji_reaction) - .emojiPicker( - isPresented: $isReactionsVisible, - selectedEmoji: $settings.default_emoji_reaction, - arrowDirection: .up, - isDismissAfterChoosing: true - ) .onTapGesture { isReactionsVisible = true } @@ -31,43 +29,23 @@ struct ReactionsSettingsView: View { } .navigationTitle(NSLocalizedString("Reactions", comment: "Title of emoji reactions view")) .navigationBarTitleDisplayMode(.large) + .sheet(isPresented: $isReactionsVisible) { + NavigationView { + EmojiPickerView(selectedEmoji: $selectedEmoji, emojiProvider: damus_state.emoji_provider) + } + .presentationDetents([.medium, .large]) + } + .onChange(of: selectedEmoji) { newEmoji in + guard let newEmoji else { + return + } + settings.default_emoji_reaction = newEmoji.value + } } } -/// From: https://stackoverflow.com/a/39425959 -extension Character { - /// A simple emoji is one scalar and presented to the user as an Emoji - var isSimpleEmoji: Bool { - guard let firstScalar = unicodeScalars.first else { return false } - return firstScalar.properties.isEmoji && firstScalar.value > 0x238C - } - - /// Checks if the scalars will be merged into an emoji - var isCombinedIntoEmoji: Bool { unicodeScalars.count > 1 && unicodeScalars.first?.properties.isEmoji ?? false } - - var isEmoji: Bool { isSimpleEmoji || isCombinedIntoEmoji } -} - -extension String { - var isSingleEmoji: Bool { count == 1 && containsEmoji } - - var containsEmoji: Bool { contains { $0.isEmoji } } - - var containsOnlyEmoji: Bool { !isEmpty && !contains { !$0.isEmoji } } - - var emojiString: String { emojis.map { String($0) }.reduce("", +) } - - var emojis: [Character] { filter { $0.isEmoji } } - - var emojiScalars: [UnicodeScalar] { filter { $0.isEmoji }.flatMap { $0.unicodeScalars } } -} - -func isValidEmoji(_ string: String) -> Bool { - return string.isSingleEmoji -} - struct ReactionsSettingsView_Previews: PreviewProvider { static var previews: some View { - ReactionsSettingsView(settings: UserSettingsStore()) + ReactionsSettingsView(settings: UserSettingsStore(), damus_state: test_damus_state) } } diff --git a/damusTests/Mocking/MockDamusState.swift b/damusTests/Mocking/MockDamusState.swift @@ -7,6 +7,7 @@ import Foundation @testable import damus +import EmojiPicker // Generates a test damus state with configurable mock parameters func generate_test_damus_state( @@ -50,7 +51,9 @@ func generate_test_damus_state( music: .init(onChange: {_ in }), video: .init(), ndb: ndb, - quote_reposts: .init(our_pubkey: our_pubkey) ) + quote_reposts: .init(our_pubkey: our_pubkey), + emoji_provider: DefaultEmojiProvider() + ) return damus }