Add multiple reaction support

Changelog-Added: Add support for multiple reactions
Closes: https://github.com/damus-io/damus/issues/1335
This commit is contained in:
Suhail Saqan
2023-07-29 09:42:59 -07:00
committed by William Casarin
parent 815f4d4a96
commit d11cd76e6a
8 changed files with 459 additions and 27 deletions

View File

@@ -0,0 +1,48 @@
//
// 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
}
}
}
}
}

View File

@@ -0,0 +1,79 @@
//
// 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 {
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)
}
}
}

View File

@@ -0,0 +1,151 @@
//
// ReactionsSettingsView.swift
// damus
//
// Created by Suhail Saqan on 7/3/23.
//
import SwiftUI
import Combine
let default_emoji_reactions = ["🤣", "🤙", "", "💜", "🔥", "😀", "😃", "😄", "🥶"]
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)
}
var body: some View {
Form {
Section {
AddEmojiView(emoji: $new_emoji)
} 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(settings.emoji_reactions, id: \.self) { emoji in
EmojiListItemView(settings: settings, emoji: emoji, recommended: false, showActionButtons: $showActionButtons)
}
} 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(recommended), id: \.self) { emoji in
EmojiListItemView(settings: settings, emoji: emoji, recommended: true, showActionButtons: $showActionButtons)
}
} header: {
Text("Recommended Emojis", comment: "Section title for recommend emojis")
.font(.system(size: 18, weight: .heavy))
.padding(.bottom, 5)
}
}
}
.navigationTitle(NSLocalizedString("Reactions", comment: "Title of emoji reactions view"))
.navigationBarTitleDisplayMode(.large)
.toolbar {
if showActionButtons {
Button("Done") {
showActionButtons.toggle()
}
} else {
Button("Edit") {
showActionButtons.toggle()
}
}
}
}
// 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
}
}
/// 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())
}
}