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
This commit is contained in:
Daniel D’Aquino
2024-06-24 11:38:31 -07:00
parent 23c3130a82
commit 529ee63f29
10 changed files with 109 additions and 106 deletions

View File

@@ -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 */; };
@@ -1485,10 +1485,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;
};
@@ -2866,7 +2866,7 @@
4C649880286E0EE300EAE2B3 /* secp256k1 */,
4C06670328FC7EC500038D2A /* Kingfisher */,
4C27C9312A64766F007DBC75 /* MarkdownUI */,
3AFE89C22BD4156F00AD31EF /* MCEmojiPicker */,
3A0A30BA2C21397A00F8C9BC /* EmojiPicker */,
D78DB8582C1CE9CA00F0AB12 /* SwipeActions */,
);
productName = damus;
@@ -3007,7 +3007,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 */;
@@ -4316,12 +4316,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" */ = {
@@ -4375,10 +4375,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;

View File

@@ -1,6 +1,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",
@@ -19,15 +37,6 @@
"version" : "7.6.1"
}
},
{
"identity" : "mcemojipicker",
"kind" : "remoteSourceControl",
"location" : "https://github.com/izyumkin/MCEmojiPicker",
"state" : {
"revision" : "e0b4903b75ae1cc418d276d84d1cb946b8a1d73c",
"version" : "1.2.3"
}
},
{
"identity" : "secp256k1.swift",
"kind" : "remoteSourceControl",
@@ -36,6 +45,15 @@
"revision" : "40b4b38b3b1c83f7088c76189a742870e0ca06a9"
}
},
{
"identity" : "swift-collections",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git",
"state" : {
"revision" : "ee97538f5b81ae89698fd95938896dec5217b148",
"version" : "1.1.1"
}
},
{
"identity" : "swift-markdown-ui",
"kind" : "remoteSourceControl",
@@ -62,6 +80,15 @@
"version" : "509.0.0"
}
},
{
"identity" : "swift-trie",
"kind" : "remoteSourceControl",
"location" : "https://github.com/tyiu/swift-trie",
"state" : {
"revision" : "4c50bff6c168f74425f70476be62a072980d2da7",
"version" : "0.1.2"
}
},
{
"identity" : "swipeactions",
"kind" : "remoteSourceControl",

View File

@@ -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!

View File

@@ -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)
)
}
}

View File

@@ -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)
)
/*

View File

@@ -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):

View File

@@ -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)
}
}
}

View File

@@ -6,7 +6,8 @@
//
import SwiftUI
import MCEmojiPicker
import EmojiKit
import EmojiPicker
import SwipeActions
fileprivate let CORNER_RADIUS: CGFloat = 10
@@ -34,7 +35,7 @@ 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
}
}

View File

@@ -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)
}
}

View File

@@ -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
}