Change reactions to use a native looking emoji picker
Changelog-Changed: Change reactions to use a native looking emoji picker Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
@@ -6,8 +6,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
import MCEmojiPicker
|
||||
|
||||
struct EventActionBar: View {
|
||||
let damus_state: DamusState
|
||||
@@ -20,6 +19,8 @@ struct EventActionBar: View {
|
||||
@State var show_share_action: Bool = false
|
||||
@State var show_repost_action: Bool = false
|
||||
|
||||
@State private var isOnTopHalfOfScreen: Bool = false
|
||||
|
||||
@ObservedObject var bar: ActionBarModel
|
||||
|
||||
init(damus_state: DamusState, event: NostrEvent, bar: ActionBarModel? = nil) {
|
||||
@@ -72,7 +73,7 @@ struct EventActionBar: View {
|
||||
Spacer()
|
||||
|
||||
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) { emoji in
|
||||
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
|
||||
if bar.liked {
|
||||
//notify(.delete, bar.our_like)
|
||||
} else {
|
||||
@@ -135,8 +136,22 @@ 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) {
|
||||
guard let keypair = damus_state.keypair.to_full(),
|
||||
let like_ev = make_like_event(keypair: keypair, liked: event, content: emoji) else {
|
||||
@@ -168,15 +183,17 @@ 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
|
||||
@State private var showReactionsBG = 0
|
||||
@State private var showEmojis: [Int] = []
|
||||
@State private var rotateThumb = -45
|
||||
|
||||
@State private var isReactionsVisible = false
|
||||
|
||||
@State private var selectedEmoji: String = ""
|
||||
|
||||
// Following four are Shaka animation properties
|
||||
let timer = Timer.publish(every: 0.10, on: .main, in: .common).autoconnect()
|
||||
@State private var shouldAnimate = false
|
||||
@@ -228,7 +245,15 @@ struct LikeButton: View {
|
||||
amountOfAngleIncrease = 20.0
|
||||
}
|
||||
})
|
||||
.overlay(reactionsOverlay())
|
||||
.emojiPicker(
|
||||
isPresented: $isReactionsVisible,
|
||||
selectedEmoji: $selectedEmoji,
|
||||
arrowDirection: isOnTopHalfOfScreen ? .down : .up,
|
||||
isDismissAfterChoosing: true
|
||||
)
|
||||
.onChange(of: selectedEmoji) { newSelectedEmoji in
|
||||
self.action(newSelectedEmoji)
|
||||
}
|
||||
}
|
||||
|
||||
func shakaAnimationLogic() {
|
||||
@@ -251,110 +276,11 @@ struct LikeButton: View {
|
||||
}
|
||||
}
|
||||
|
||||
func reactionsOverlay() -> some View {
|
||||
Group {
|
||||
if isReactionsVisible {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.frame(width: calculateOverlayWidth(), height: 50)
|
||||
.foregroundColor(DamusColors.black)
|
||||
.scaleEffect(Double(showReactionsBG), anchor: .topTrailing)
|
||||
.animation(
|
||||
.interpolatingSpring(stiffness: 170, damping: 15).delay(0.05),
|
||||
value: showReactionsBG
|
||||
)
|
||||
.overlay(
|
||||
Rectangle()
|
||||
.foregroundColor(Color.white.opacity(0.2))
|
||||
.frame(width: calculateOverlayWidth(), height: 50)
|
||||
.clipShape(
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
)
|
||||
)
|
||||
.overlay(reactions())
|
||||
}
|
||||
.offset(y: -40)
|
||||
.onTapGesture {
|
||||
withAnimation(.easeOut(duration: 0.2)) {
|
||||
isReactionsVisible = false
|
||||
showReactionsBG = 0
|
||||
}
|
||||
showEmojis = []
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func calculateOverlayWidth() -> CGFloat {
|
||||
let maxWidth: CGFloat = 250
|
||||
let numberOfEmojis = emojis.count
|
||||
let minimumWidth: CGFloat = 75
|
||||
|
||||
if numberOfEmojis > 0 {
|
||||
let emojiWidth: CGFloat = 25
|
||||
let padding: CGFloat = 15
|
||||
let buttonWidth: CGFloat = 18
|
||||
let buttonPadding: CGFloat = 20
|
||||
|
||||
let totalWidth = CGFloat(numberOfEmojis) * (emojiWidth + padding) + buttonWidth + buttonPadding
|
||||
return min(maxWidth, max(minimumWidth, totalWidth))
|
||||
} else {
|
||||
return minimumWidth
|
||||
}
|
||||
}
|
||||
|
||||
func reactions() -> some View {
|
||||
HStack {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 15) {
|
||||
ForEach(emojis, id: \.self) { emoji in
|
||||
if let index = emojis.firstIndex(of: emoji) {
|
||||
let scale = index < showEmojis.count ? showEmojis[index] : 0
|
||||
Text(emoji)
|
||||
.font(.system(size: 25))
|
||||
.scaleEffect(Double(scale))
|
||||
.onTapGesture {
|
||||
emojiTapped(emoji)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.leading, 10)
|
||||
}
|
||||
Button(action: {
|
||||
withAnimation(.easeOut(duration: 0.2)) {
|
||||
isReactionsVisible = false
|
||||
showReactionsBG = 0
|
||||
}
|
||||
showEmojis = []
|
||||
}) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.system(size: 18))
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.padding(.trailing, 7.5)
|
||||
}
|
||||
}
|
||||
|
||||
// When reaction button is long pressed, it displays the multiple emojis overlay and displays the user's selected emojis with an animation
|
||||
private func reactionLongPressed() {
|
||||
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
|
||||
showEmojis = Array(repeating: 0, count: emojis.count) // Initialize the showEmojis array
|
||||
|
||||
for (index, _) in emojis.enumerated() {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1 * Double(index)) {
|
||||
withAnimation(.interpolatingSpring(stiffness: 170, damping: 8)) {
|
||||
if index < showEmojis.count {
|
||||
showEmojis[index] = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isReactionsVisible = true
|
||||
showReactionsBG = 1
|
||||
}
|
||||
|
||||
private func emojiTapped(_ emoji: String) {
|
||||
@@ -364,9 +290,7 @@ struct LikeButton: View {
|
||||
|
||||
withAnimation(.easeOut(duration: 0.2)) {
|
||||
isReactionsVisible = false
|
||||
showReactionsBG = 0
|
||||
}
|
||||
showEmojis = []
|
||||
|
||||
withAnimation(Animation.easeOut(duration: 0.15)) {
|
||||
shouldAnimate = true
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
//
|
||||
// AddEmojiView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Suhail Saqan on 7/16/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AddEmojiView: View {
|
||||
@Binding var emoji: String
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .leading) {
|
||||
HStack{
|
||||
TextField(NSLocalizedString("⚡", comment: "Placeholder example for an emoji reaction"), text: $emoji)
|
||||
.padding(2)
|
||||
.padding(.leading, 25)
|
||||
.opacity(emoji == "" ? 0.5 : 1)
|
||||
.autocorrectionDisabled(true)
|
||||
.textInputAutocapitalization(.never)
|
||||
.onChange(of: emoji) { newEmoji in
|
||||
if let lastEmoji = newEmoji.last.map(String.init), isValidEmoji(lastEmoji) {
|
||||
self.emoji = lastEmoji
|
||||
} else {
|
||||
self.emoji = ""
|
||||
}
|
||||
}
|
||||
|
||||
Label("", image: "close-circle")
|
||||
.foregroundColor(.accentColor)
|
||||
.padding(.trailing, -25.0)
|
||||
.opacity((emoji == "") ? 0.0 : 1.0)
|
||||
.onTapGesture {
|
||||
self.emoji = ""
|
||||
}
|
||||
}
|
||||
|
||||
Label("", image: "copy2")
|
||||
.padding(.leading, -10)
|
||||
.onTapGesture {
|
||||
if let pastedEmoji = UIPasteboard.general.string {
|
||||
self.emoji = pastedEmoji
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
//
|
||||
// EmojiListItemView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Suhail Saqan on 7/16/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct EmojiListItemView: View {
|
||||
@ObservedObject var settings: UserSettingsStore
|
||||
|
||||
let emoji: String
|
||||
let recommended: Bool
|
||||
|
||||
@Binding var showActionButtons: Bool
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
HStack {
|
||||
if showActionButtons {
|
||||
if recommended {
|
||||
AddButton()
|
||||
} else {
|
||||
RemoveButton()
|
||||
}
|
||||
}
|
||||
|
||||
Text(emoji)
|
||||
}
|
||||
}
|
||||
.swipeActions {
|
||||
if !recommended {
|
||||
RemoveButton()
|
||||
.tint(.red)
|
||||
} else {
|
||||
AddButton()
|
||||
.tint(.green)
|
||||
}
|
||||
}
|
||||
.contextMenu {
|
||||
if !showActionButtons {
|
||||
CopyAction(emoji: emoji)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func CopyAction(emoji: String) -> some View {
|
||||
Button {
|
||||
UIPasteboard.general.setValue(emoji, forPasteboardType: "public.plain-text")
|
||||
} label: {
|
||||
Label(NSLocalizedString("Copy", comment: "Button to copy an emoji reaction"), image: "copy2")
|
||||
}
|
||||
}
|
||||
|
||||
func RemoveButton() -> some View {
|
||||
Button(action: {
|
||||
if let index = settings.emoji_reactions.firstIndex(of: emoji) {
|
||||
settings.emoji_reactions.remove(at: index)
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "minus.circle")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundColor(.red)
|
||||
.padding(.leading, -5)
|
||||
}
|
||||
}
|
||||
|
||||
func AddButton() -> some View {
|
||||
Button(action: {
|
||||
settings.emoji_reactions.append(emoji)
|
||||
}) {
|
||||
Image(systemName: "plus.circle")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundColor(.green)
|
||||
.padding(.leading, -5)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,114 +6,31 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
import MCEmojiPicker
|
||||
|
||||
struct ReactionsSettingsView: View {
|
||||
@ObservedObject var settings: UserSettingsStore
|
||||
|
||||
@State var new_emoji: String = ""
|
||||
@State private var showActionButtons = false
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
var recommended: [String] {
|
||||
return getMissingRecommendedEmojis(added: settings.emoji_reactions)
|
||||
}
|
||||
|
||||
@State private var isReactionsVisible: Bool = false
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
AddEmojiView(emoji: $new_emoji)
|
||||
Text(settings.default_emoji_reaction)
|
||||
.emojiPicker(
|
||||
isPresented: $isReactionsVisible,
|
||||
selectedEmoji: $settings.default_emoji_reaction,
|
||||
arrowDirection: .up,
|
||||
isDismissAfterChoosing: true
|
||||
)
|
||||
.onTapGesture {
|
||||
isReactionsVisible = true
|
||||
}
|
||||
} header: {
|
||||
Text(NSLocalizedString("Add Emoji", comment: "Label for section for adding an emoji to the reactions list."))
|
||||
.font(.system(size: 18, weight: .heavy))
|
||||
.padding(.bottom, 5)
|
||||
} footer: {
|
||||
HStack {
|
||||
Spacer()
|
||||
if !new_emoji.isEmpty {
|
||||
Button(NSLocalizedString("Cancel", comment: "Button to cancel out of view adding user inputted emoji.")) {
|
||||
new_emoji = ""
|
||||
}
|
||||
.font(.system(size: 14, weight: .bold))
|
||||
.frame(width: 80, height: 30)
|
||||
.foregroundColor(.white)
|
||||
.background(LINEAR_GRADIENT)
|
||||
.clipShape(Capsule())
|
||||
.padding(EdgeInsets(top: 15, leading: 0, bottom: 0, trailing: 0))
|
||||
|
||||
Button(NSLocalizedString("Add", comment: "Button to confirm adding user inputted emoji.")) {
|
||||
if isValidEmoji(new_emoji) {
|
||||
settings.emoji_reactions.append(new_emoji)
|
||||
new_emoji = ""
|
||||
}
|
||||
}
|
||||
.font(.system(size: 14, weight: .bold))
|
||||
.frame(width: 80, height: 30)
|
||||
.foregroundColor(.white)
|
||||
.background(LINEAR_GRADIENT)
|
||||
.clipShape(Capsule())
|
||||
.padding(EdgeInsets(top: 15, leading: 0, bottom: 0, trailing: 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Picker(NSLocalizedString("Select default emoji", comment: "Prompt selection of user's default emoji reaction"),
|
||||
selection: $settings.default_emoji_reaction) {
|
||||
ForEach(settings.emoji_reactions, id: \.self) { emoji in
|
||||
Text(emoji)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
List {
|
||||
ForEach(Array(zip(settings.emoji_reactions, 1...)), id: \.1) { tup in
|
||||
EmojiListItemView(settings: settings, emoji: tup.0, recommended: false, showActionButtons: $showActionButtons)
|
||||
}
|
||||
.onMove(perform: showActionButtons ? move: nil)
|
||||
}
|
||||
} header: {
|
||||
Text("Emoji Reactions", comment: "Section title for emoji reactions that are currently added.")
|
||||
.font(.system(size: 18, weight: .heavy))
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
|
||||
if recommended.count > 0 {
|
||||
Section {
|
||||
List(Array(zip(recommended, 1...)), id: \.1) { tup in
|
||||
EmojiListItemView(settings: settings, emoji: tup.0, recommended: true, showActionButtons: $showActionButtons)
|
||||
}
|
||||
} header: {
|
||||
Text("Recommended Emojis", comment: "Section title for recommend emojis")
|
||||
.font(.system(size: 18, weight: .heavy))
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
Text(NSLocalizedString("Select default emoji", comment: "Prompt selection of user's default emoji reaction"))
|
||||
}
|
||||
}
|
||||
.navigationTitle(NSLocalizedString("Reactions", comment: "Title of emoji reactions view"))
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.toolbar {
|
||||
if showActionButtons {
|
||||
Button("Done") {
|
||||
showActionButtons.toggle()
|
||||
}
|
||||
} else {
|
||||
Button("Edit") {
|
||||
showActionButtons.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func move(from: IndexSet, to: Int) {
|
||||
settings.emoji_reactions.move(fromOffsets: from, toOffset: to)
|
||||
}
|
||||
|
||||
// Returns the emojis that are in the recommended list but the user has not added yet
|
||||
func getMissingRecommendedEmojis(added: [String], recommended: [String] = default_emoji_reactions) -> [String] {
|
||||
let addedSet = Set(added)
|
||||
let missingEmojis = recommended.filter { !addedSet.contains($0) }
|
||||
return missingEmojis
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user