Przeglądaj źródła

wallet: customize emoji and font

darkfi 1 rok temu
rodzic
commit
2a922df3ce

+ 2 - 15
bin/darkwallet/Cargo.lock

@@ -1316,7 +1316,6 @@ dependencies = [
  "darkfi-serial",
  "dirs",
  "easy-parallel",
- "evgrd",
  "file-rotate",
  "freetype-rs",
  "futures",
@@ -1327,6 +1326,7 @@ dependencies = [
  "miniquad",
  "parking_lot 0.12.3",
  "rand",
+ "regex",
  "rusqlite",
  "semver",
  "simplelog",
@@ -1802,19 +1802,6 @@ dependencies = [
  "pin-project-lite",
 ]
 
-[[package]]
-name = "evgrd"
-version = "0.5.1"
-dependencies = [
- "blake3",
- "darkfi",
- "darkfi-serial",
- "log",
- "sled-overlay",
- "smol",
- "url",
-]
-
 [[package]]
 name = "exr"
 version = "1.73.0"
@@ -3054,7 +3041,7 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
 [[package]]
 name = "miniquad"
 version = "0.4.7"
-source = "git+https://github.com/not-fl3/miniquad#2f716f5664551c28e8d8c0e90187e1b629cffc94"
+source = "git+https://github.com/not-fl3/miniquad#8b8dc6225693ef8c461a0cd44724e33baa9d9fe0"
 dependencies = [
  "libc",
  "ndk-sys",

+ 1 - 7
bin/darkwallet/Cargo.toml

@@ -8,10 +8,6 @@ license = "AGPL-3.0-only"
 homepage = "https://dark.fi"
 repository = "https://codeberg.org/darkrenaissance/darkfi"
 
-#[[bin]]
-#name = "drawsim"
-#path = "bin/drawsim.rs"
-
 [dependencies]
 miniquad = { git = "https://github.com/not-fl3/miniquad" }
 
@@ -24,11 +20,8 @@ freetype-rs = { version = "0.37.0", features = ["bundled"] }
 image = "0.25.2"
 log = { version = "0.4", features = ["release_max_level_info"] }
 glam = "0.29.0"
-#zmq = "0.10.0"
-#async_zmq = "0.4.0"
 zeromq = { version = "0.4.1", default-features = false, features = ["async-std-runtime", "all-transport"] }
 darkfi = {path = "../../", features = ["async-daemonize", "event-graph", "net", "util", "system", "zk"]}
-evgrd = {path = "../../script/evgrd/"}
 #darkfi-sdk = {path = "../../src/sdk", features = ["async"]}
 darkfi-serial = {version = "0.4.2", features = ["async"]}
 thiserror = "1.0.64"
@@ -53,6 +46,7 @@ simplelog = "0.12.2"
 # For log files
 file-rotate = "0.7.6"
 parking_lot = { version = "0.12", features = ["nightly"] }
+regex = "1.11"
 
 [features]
 emulate-android = []

+ 2 - 2
bin/darkwallet/Makefile

@@ -12,8 +12,8 @@ SRC = \
 
 #FEATURES = --features=emulate-android
 #FEATURES = --features=enable-plugins
-FEATURES = --features=enable-netdebug,enable-plugins
-#FEATURES = --features=enable-netdebug
+#FEATURES = --features=enable-netdebug,enable-plugins
+FEATURES = --features=enable-netdebug
 
 all: $(SRC) fonts
 	# You can either install cargo-limit or just s/lbuild/build

+ 2 - 1
bin/darkwallet/java/MainActivity.java

@@ -19,7 +19,8 @@ public String getAppDataPath() {
     return getApplicationContext().getDataDir().getAbsolutePath();
 }
 public String getExternalStoragePath() {
-    return Environment.getExternalStorageDirectory().getAbsolutePath();
+    return getApplicationContext().getExternalFilesDir(null).getAbsolutePath();
+    //return Environment.getExternalStorageDirectory().getAbsolutePath();
 }
 
 public int getKeyboardHeight() {

+ 3 - 1
bin/darkwallet/src/app/schema/mod.rs

@@ -68,8 +68,9 @@ mod ui_consts {
     pub use super::android_ui_consts::*;
 
     pub fn get_chatdb_path() -> PathBuf {
-        get_appdata_path().join("chatdb")
+        get_external_storage_path().join("chatdb")
     }
+
     pub fn get_first_time_filename() -> PathBuf {
         get_appdata_path().join("first_time")
     }
@@ -88,6 +89,7 @@ mod desktop_paths {
     pub fn get_chatdb_path() -> PathBuf {
         dirs::data_local_dir().unwrap().join("darkfi/wallet/chatdb")
     }
+
     pub fn get_first_time_filename() -> PathBuf {
         dirs::cache_dir().unwrap().join("darkfi/wallet/first_time")
     }

+ 1 - 1
bin/darkwallet/src/plugin/darkirc.rs

@@ -66,7 +66,7 @@ mod paths {
     use std::path::PathBuf;
 
     pub fn get_evgrdb_path() -> PathBuf {
-        get_appdata_path().join("evgr")
+        get_external_storage_path().join("evgr")
     }
 
     pub fn nick_filename() -> PathBuf {

+ 57 - 3
bin/darkwallet/src/text/mod.rs

@@ -25,7 +25,9 @@ use harfbuzz_sys::{
 };
 use std::{
     collections::HashMap,
+    ffi::OsStr,
     os,
+    path::PathBuf,
     sync::{Arc, Mutex as SyncMutex, Weak},
 };
 
@@ -45,6 +47,15 @@ pub const EMOJI_SCALE_FACT: f32 = 1.6;
 // How much of the emoji is above baseline?
 pub const EMOJI_PROP_ABOVE_BASELINE: f32 = 0.8;
 
+#[cfg(target_os = "android")]
+fn custom_font_path() -> PathBuf {
+    get_external_storage_path().join("font")
+}
+#[cfg(not(target_os = "android"))]
+fn custom_font_path() -> PathBuf {
+    dirs::data_local_dir().unwrap().join("darkfi/font")
+}
+
 // From https://sourceforge.net/projects/freetype/files/freetype2/2.6/
 //
 // * An `FT_Face' object can only be safely used from one thread at
@@ -153,21 +164,56 @@ impl TextShaperInternal {
 
 pub struct TextShaper {
     intern: SyncMutex<TextShaperInternal>,
+    fonts_data: Vec<Vec<u8>>,
 }
 
 impl TextShaper {
     pub fn new() -> Arc<Self> {
         let ftlib = freetype::Library::init().unwrap();
 
+        let mut fonts_data = vec![];
+        if let Ok(read_dir) = std::fs::read_dir(custom_font_path()) {
+            for entry in read_dir {
+                let Ok(entry) = entry else {
+                    warn!(target: "text", "Skipping unknown in custom font path");
+                    continue
+                };
+                let font_path = entry.path();
+                if font_path.is_dir() {
+                    warn!(target: "text", "Skipping {font_path:?} in custom font path: is directory");
+                    continue
+                }
+                let Some(font_ext) = font_path.extension().and_then(OsStr::to_str) else {
+                    warn!(target: "text", "Skipping {font_path:?} in custom font path: missing file extension");
+                    continue
+                };
+                if !["ttf", "otf"].contains(&font_ext) {
+                    warn!(target: "text", "Skipping {font_path:?} in custom font path: unsupported file extension (supported: ttf, otf)");
+                    continue
+                }
+                let font_data: Vec<u8> = match std::fs::read(&font_path) {
+                    Ok(font_data) => font_data,
+                    Err(err) => {
+                        warn!(target: "text", "Unexpected error loading {font_path:?} in custom font path: {err}");
+                        continue
+                    }
+                };
+                info!(target: "text", "Loaded custom font: {font_path:?}");
+                fonts_data.push(font_data);
+            }
+        }
+        fonts_data.reserve_exact(fonts_data.len() + 2);
+
         let mut faces = vec![];
 
         let font_data = include_bytes!("../../ibm-plex-mono-regular.otf") as &[u8];
         let ft_face = ftlib.new_memory_face2(font_data, 0).unwrap();
         faces.push(ft_face);
 
-        //let font_data = include_bytes!("../../darkfi-custom-emoji.ttf") as &[u8];
-        //let ft_face = ftlib.new_memory_face2(font_data, 0).unwrap();
-        //faces.push(ft_face);
+        for font_data in &fonts_data {
+            let face = unsafe { Self::load_font_face(&ftlib, font_data) };
+            faces.push(face);
+        }
 
         let font_data = include_bytes!("../../NotoColorEmoji.ttf") as &[u8];
         let ft_face = ftlib.new_memory_face2(font_data, 0).unwrap();
@@ -178,9 +224,17 @@ impl TextShaper {
                 font_faces: FtFaces(faces),
                 cache: HashMap::new(),
             }),
+            fonts_data,
         })
     }
 
+    /// Beware: recasts font_data as static. Make sure data outlives the face.
+    unsafe fn load_font_face(ftlib: &freetype::Library, font_data: &[u8]) -> FreetypeFace {
+        let font_data = &*(font_data as *const _);
+        let ft_face = ftlib.new_memory_face2(font_data, 0).unwrap();
+        ft_face
+    }
+
     pub fn shape(&self, mut text: String, font_size: f32, window_scale: f32) -> Vec<Glyph> {
         //debug!(target: "text", "shape('{}', {})", text, font_size);
         if text.is_empty() {

+ 589 - 0
bin/darkwallet/src/ui/emoji_picker/default.rs

@@ -0,0 +1,589 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2024 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+pub fn create_default_emoji_list() -> Vec<String> {
+    vec![
+        "😀",
+        "😃",
+        "😄",
+        "😁",
+        "😆",
+        "😅",
+        "🤣",
+        "😂",
+        "🙂",
+        "🙃",
+        "🫠",
+        "😉",
+        "😊",
+        "😇",
+        "🥰",
+        "😍",
+        "🤩",
+        "😘",
+        "😗",
+        "😚",
+        "😙",
+        "🥲",
+        "😋",
+        "😛",
+        "😜",
+        "🤪",
+        "😝",
+        "🤑",
+        "🤗",
+        "🤭",
+        "🫢",
+        "🫣",
+        "🤫",
+        "🤔",
+        "🫡",
+        "🤐",
+        "🤨",
+        "😐",
+        "😑",
+        "😶",
+        "😏",
+        "😒",
+        "🙄",
+        "😬",
+        "🤥",
+        "🫨",
+        "😌",
+        "😔",
+        "😪",
+        "🤤",
+        "😴",
+        "😷",
+        "🤒",
+        "🤕",
+        "🤢",
+        "🤮",
+        "🤧",
+        "🥵",
+        "🥶",
+        "🥴",
+        "😵",
+        "🤯",
+        "🤠",
+        "🥳",
+        "🥸",
+        "😎",
+        "🤓",
+        "🧐",
+        "😕",
+        "🫤",
+        "😟",
+        "🙁",
+        "😮",
+        "😯",
+        "😲",
+        "😳",
+        "🥺",
+        "🥹",
+        "😦",
+        "😧",
+        "😨",
+        "😰",
+        "😥",
+        "😢",
+        "😭",
+        "😱",
+        "😖",
+        "😣",
+        "😞",
+        "😓",
+        "😩",
+        "😫",
+        "🥱",
+        "😤",
+        "😡",
+        "😠",
+        "🤬",
+        "😈",
+        "👿",
+        "💀",
+        "💩",
+        "🤡",
+        "👹",
+        "👺",
+        "👻",
+        "👽",
+        "👾",
+        "🤖",
+        "😺",
+        "😸",
+        "😹",
+        "😻",
+        "😼",
+        "😽",
+        "🙀",
+        "😿",
+        "😾",
+        "🙈",
+        "🙉",
+        "🙊",
+        "💘",
+        "💝",
+        "💖",
+        "💗",
+        "💓",
+        "💞",
+        "💕",
+        "💔",
+        "🩷",
+        "🧡",
+        "💛",
+        "💚",
+        "💙",
+        "🩵",
+        "💜",
+        "🤎",
+        "🖤",
+        "🩶",
+        "🤍",
+        "💋",
+        "💯",
+        "💢",
+        "💥",
+        "💫",
+        "💦",
+        "💨",
+        "💬",
+        "🗨",
+        "🗯",
+        "💭",
+        "💤",
+        "👋",
+        "🤚",
+        "✋",
+        "🖖",
+        "🫱",
+        "🫲",
+        "🫳",
+        "🫴",
+        "🫷",
+        "🫸",
+        "👌",
+        "🤌",
+        "🤏",
+        "🤞",
+        "🫰",
+        "🤟",
+        "🤘",
+        "🤙",
+        "👈",
+        "👉",
+        "👆",
+        "🖕",
+        "👇",
+        "🫵",
+        "👍",
+        "👎",
+        "✊",
+        "👊",
+        "🤛",
+        "🤜",
+        "👏",
+        "🙌",
+        "🫶",
+        "👐",
+        "🤲",
+        "🤝",
+        "🙏",
+        "💅",
+        "🤳",
+        "💪",
+        "🦾",
+        "🦿",
+        "🦵",
+        "🦶",
+        "👂",
+        "🦻",
+        "👃",
+        "🧠",
+        "🫀",
+        "🫁",
+        "🦷",
+        "🦴",
+        "👀",
+        "👁",
+        "👅",
+        "👄",
+        "🫦",
+        "👶",
+        "🧒",
+        "👦",
+        "👧",
+        "🧑",
+        "👱",
+        "👨",
+        "🧔",
+        "👩",
+        "🧓",
+        "👴",
+        "👵",
+        "🙍",
+        "🙎",
+        "🙅",
+        "🙆",
+        "💁",
+        "🙋",
+        "🧏",
+        "🙇",
+        "🤦",
+        "🤷",
+        "👮",
+        "💂",
+        "🥷",
+        "👷",
+        "🫅",
+        "🤴",
+        "👸",
+        "👳",
+        "👲",
+        "🧕",
+        "🤵",
+        "👰",
+        "🤰",
+        "🫃",
+        "🫄",
+        "🤱",
+        "👼",
+        "🎅",
+        "🤶",
+        "🦸",
+        "🦹",
+        "🧙",
+        "🧚",
+        "🧛",
+        "🧜",
+        "🧝",
+        "🧞",
+        "🧟",
+        "🧌",
+        "💆",
+        "💇",
+        "🚶",
+        "🧍",
+        "🧎",
+        "🏃",
+        "💃",
+        "🕺",
+        "🕴",
+        "👯",
+        "🧖",
+        "🧗",
+        "🤺",
+        "🏇",
+        "⛷",
+        "🏂",
+        "🏌",
+        "🏄",
+        "🚣",
+        "🏊",
+        "⛹",
+        "🏋",
+        "🚴",
+        "🚵",
+        "🤸",
+        "🤼",
+        "🤽",
+        "🤾",
+        "🤹",
+        "🧘",
+        "🛀",
+        "🛌",
+        "👭",
+        "👫",
+        "👬",
+        "💏",
+        "💑",
+        "🗣",
+        "👤",
+        "👥",
+        "🫂",
+        "👪",
+        "👣",
+        "🐵",
+        "🐒",
+        "🦍",
+        "🦧",
+        "🐶",
+        "🐕",
+        "🦮",
+        "🐩",
+        "🐺",
+        "🦊",
+        "🦝",
+        "🐱",
+        "🐈",
+        "🦁",
+        "🐯",
+        "🐅",
+        "🐆",
+        "🐴",
+        "🫎",
+        "🫏",
+        "🐎",
+        "🦄",
+        "🦓",
+        "🦌",
+        "🦬",
+        "🐮",
+        "🐂",
+        "🐃",
+        "🐄",
+        "🐷",
+        "🐖",
+        "🐗",
+        "🐽",
+        "🐏",
+        "🐑",
+        "🐐",
+        "🐪",
+        "🐫",
+        "🦙",
+        "🦒",
+        "🐘",
+        "🦣",
+        "🦏",
+        "🦛",
+        "🐭",
+        "🐁",
+        "🐀",
+        "🐹",
+        "🐰",
+        "🐇",
+        "🐿",
+        "🦫",
+        "🦔",
+        "🦇",
+        "🐻",
+        "🐨",
+        "🐼",
+        "🦥",
+        "🦦",
+        "🦨",
+        "🦘",
+        "🦡",
+        "🐾",
+        "🦃",
+        "🐔",
+        "🐓",
+        "🐣",
+        "🐤",
+        "🐥",
+        "🐦",
+        "🐧",
+        "🕊",
+        "🦅",
+        "🦆",
+        "🦢",
+        "🦉",
+        "🦤",
+        "🪶",
+        "🦩",
+        "🦚",
+        "🦜",
+        "🪽",
+        "🪿",
+        "🐸",
+        "🐊",
+        "🐢",
+        "🦎",
+        "🐍",
+        "🐲",
+        "🐉",
+        "🦕",
+        "🦖",
+        "🐳",
+        "🐋",
+        "🐬",
+        "🦭",
+        "🐟",
+        "🐠",
+        "🐡",
+        "🦈",
+        "🐙",
+        "🐚",
+        "🪸",
+        "🪼",
+        "🦀",
+        "🦞",
+        "🦐",
+        "🦑",
+        "🦪",
+        "🐌",
+        "🦋",
+        "🐛",
+        "🐜",
+        "🐝",
+        "🪲",
+        "🐞",
+        "🦗",
+        "🪳",
+        "🕷",
+        "🕸",
+        "🦂",
+        "🦟",
+        "🪰",
+        "🪱",
+        "🦠",
+        "💐",
+        "🌸",
+        "💮",
+        "🪷",
+        "🏵",
+        "🌹",
+        "🥀",
+        "🌺",
+        "🌻",
+        "🌼",
+        "🌷",
+        "🪻",
+        "🌱",
+        "🪴",
+        "🌲",
+        "🌳",
+        "🌴",
+        "🌵",
+        "🌾",
+        "🌿",
+        "☘",
+        "🍀",
+        "🍁",
+        "🍂",
+        "🍃",
+        "🪹",
+        "🪺",
+        "🍄",
+        "🍇",
+        "🍈",
+        "🍉",
+        "🍊",
+        "🍋",
+        "🍌",
+        "🍍",
+        "🥭",
+        "🍎",
+        "🍏",
+        "🍐",
+        "🍑",
+        "🍒",
+        "🍓",
+        "🫐",
+        "🥝",
+        "🍅",
+        "🫒",
+        "🥥",
+        "🥑",
+        "🍆",
+        "🥔",
+        "🥕",
+        "🌽",
+        "🌶",
+        "🫑",
+        "🥒",
+        "🥬",
+        "🥦",
+        "🧄",
+        "🧅",
+        "🥜",
+        "🫘",
+        "🌰",
+        "🫚",
+        "🫛",
+        "🍞",
+        "🥐",
+        "🥖",
+        "🫓",
+        "🥨",
+        "🥯",
+        "🥞",
+        "🧇",
+        "🧀",
+        "🍖",
+        "🍗",
+        "🥩",
+        "🥓",
+        "🍔",
+        "🍟",
+        "🍕",
+        "🌭",
+        "🥪",
+        "🌮",
+        "🌯",
+        "🫔",
+        "🥙",
+        "🧆",
+        "🥚",
+        "🍳",
+        "🥘",
+        "🍲",
+        "🫕",
+        "🥣",
+        "🥗",
+        "🍿",
+        "🧈",
+        "🧂",
+        "🥫",
+        "🍱",
+        "🍘",
+        "🍙",
+        "🍚",
+        "🍛",
+        "🍜",
+        "🍝",
+        "🍠",
+        "🍢",
+        "🍣",
+        "🍤",
+        "🍥",
+        "🥮",
+        "🍡",
+        "🥟",
+        "🥠",
+        "🥡",
+        "🍦",
+        "🍧",
+        "🍨",
+        "🍩",
+        "🍪",
+        "🎂",
+        "🍰",
+        "🧁",
+        "🥧",
+        "🍫",
+        "🍬",
+        "🍭",
+        "🍮",
+        "🍯",
+        "🍼",
+        "🥛",
+        "☕",
+        "🫖",
+        "🍵",
+        "🍶",
+        "\u{01f3f3}\u{fe0f}\u{200d}\u{26a7}\u{fe0f}",
+        "\u{01f44d}\u{01f3fe}",
+    ]
+    .into_iter()
+    .map(|s| s.to_string())
+    .collect()
+}

+ 175 - 615
bin/darkwallet/src/ui/emoji_picker/emoji.rs

@@ -1,615 +1,175 @@
-pub static EMOJI_LIST: &[&str] = &[
-    //"\u{F0000}",
-    //"\u{F0001}",
-    //"\u{F0002}",
-    //"\u{F0003}",
-    //"\u{F0004}",
-    //"\u{F0005}",
-    //"\u{F0006}",
-    //"\u{F0007}",
-    //"\u{F0008}",
-    //"\u{F0009}",
-    //"\u{F000A}",
-    //"\u{F000B}",
-    //"\u{F000C}",
-    //"\u{F000D}",
-    //"\u{F000E}",
-    //"\u{F000F}",
-    //"\u{F0010}",
-    //"\u{F0011}",
-    //"\u{F0012}",
-    //"\u{F0013}",
-    //"\u{F0014}",
-    //"\u{F0015}",
-    //"\u{F0016}",
-    //"\u{F0017}",
-    //"\u{F0018}",
-    //"\u{F0019}",
-    //"\u{F001A}",
-    //"\u{F001B}",
-    //"\u{F001C}",
-    //"\u{F001D}",
-    //"\u{F001E}",
-    //"\u{F001F}",
-    //"\u{F0020}",
-    //"\u{F0021}",
-    //"\u{F0022}",
-    //"\u{F0023}",
-    //"\u{F0024}",
-    //"\u{F0025}",
-    //"\u{F0026}",
-    //"\u{F0027}",
-    //"\u{F0028}",
-    //"\u{F0029}",
-    //"\u{F002A}",
-    //"\u{F002B}",
-    //"\u{F002C}",
-    //"\u{F002D}",
-    //"\u{F002E}",
-    //"\u{F002F}",
-    //"\u{F0030}",
-    "😀",
-    "😃",
-    "😄",
-    "😁",
-    "😆",
-    "😅",
-    "🤣",
-    "😂",
-    "🙂",
-    "🙃",
-    "🫠",
-    "😉",
-    "😊",
-    "😇",
-    "🥰",
-    "😍",
-    "🤩",
-    "😘",
-    "😗",
-    "😚",
-    "😙",
-    "🥲",
-    "😋",
-    "😛",
-    "😜",
-    "🤪",
-    "😝",
-    "🤑",
-    "🤗",
-    "🤭",
-    "🫢",
-    "🫣",
-    "🤫",
-    "🤔",
-    "🫡",
-    "🤐",
-    "🤨",
-    "😐",
-    "😑",
-    "😶",
-    "😏",
-    "😒",
-    "🙄",
-    "😬",
-    "🤥",
-    "🫨",
-    "😌",
-    "😔",
-    "😪",
-    "🤤",
-    "😴",
-    "😷",
-    "🤒",
-    "🤕",
-    "🤢",
-    "🤮",
-    "🤧",
-    "🥵",
-    "🥶",
-    "🥴",
-    "😵",
-    "🤯",
-    "🤠",
-    "🥳",
-    "🥸",
-    "😎",
-    "🤓",
-    "🧐",
-    "😕",
-    "🫤",
-    "😟",
-    "🙁",
-    "😮",
-    "😯",
-    "😲",
-    "😳",
-    "🥺",
-    "🥹",
-    "😦",
-    "😧",
-    "😨",
-    "😰",
-    "😥",
-    "😢",
-    "😭",
-    "😱",
-    "😖",
-    "😣",
-    "😞",
-    "😓",
-    "😩",
-    "😫",
-    "🥱",
-    "😤",
-    "😡",
-    "😠",
-    "🤬",
-    "😈",
-    "👿",
-    "💀",
-    "💩",
-    "🤡",
-    "👹",
-    "👺",
-    "👻",
-    "👽",
-    "👾",
-    "🤖",
-    "😺",
-    "😸",
-    "😹",
-    "😻",
-    "😼",
-    "😽",
-    "🙀",
-    "😿",
-    "😾",
-    "🙈",
-    "🙉",
-    "🙊",
-    "💘",
-    "💝",
-    "💖",
-    "💗",
-    "💓",
-    "💞",
-    "💕",
-    "💔",
-    "🩷",
-    "🧡",
-    "💛",
-    "💚",
-    "💙",
-    "🩵",
-    "💜",
-    "🤎",
-    "🖤",
-    "🩶",
-    "🤍",
-    "💋",
-    "💯",
-    "💢",
-    "💥",
-    "💫",
-    "💦",
-    "💨",
-    "💬",
-    "🗨",
-    "🗯",
-    "💭",
-    "💤",
-    "👋",
-    "🤚",
-    "✋",
-    "🖖",
-    "🫱",
-    "🫲",
-    "🫳",
-    "🫴",
-    "🫷",
-    "🫸",
-    "👌",
-    "🤌",
-    "🤏",
-    "🤞",
-    "🫰",
-    "🤟",
-    "🤘",
-    "🤙",
-    "👈",
-    "👉",
-    "👆",
-    "🖕",
-    "👇",
-    "🫵",
-    "👍",
-    "👎",
-    "✊",
-    "👊",
-    "🤛",
-    "🤜",
-    "👏",
-    "🙌",
-    "🫶",
-    "👐",
-    "🤲",
-    "🤝",
-    "🙏",
-    "💅",
-    "🤳",
-    "💪",
-    "🦾",
-    "🦿",
-    "🦵",
-    "🦶",
-    "👂",
-    "🦻",
-    "👃",
-    "🧠",
-    "🫀",
-    "🫁",
-    "🦷",
-    "🦴",
-    "👀",
-    "👁",
-    "👅",
-    "👄",
-    "🫦",
-    "👶",
-    "🧒",
-    "👦",
-    "👧",
-    "🧑",
-    "👱",
-    "👨",
-    "🧔",
-    "👩",
-    "🧓",
-    "👴",
-    "👵",
-    "🙍",
-    "🙎",
-    "🙅",
-    "🙆",
-    "💁",
-    "🙋",
-    "🧏",
-    "🙇",
-    "🤦",
-    "🤷",
-    "👮",
-    "💂",
-    "🥷",
-    "👷",
-    "🫅",
-    "🤴",
-    "👸",
-    "👳",
-    "👲",
-    "🧕",
-    "🤵",
-    "👰",
-    "🤰",
-    "🫃",
-    "🫄",
-    "🤱",
-    "👼",
-    "🎅",
-    "🤶",
-    "🦸",
-    "🦹",
-    "🧙",
-    "🧚",
-    "🧛",
-    "🧜",
-    "🧝",
-    "🧞",
-    "🧟",
-    "🧌",
-    "💆",
-    "💇",
-    "🚶",
-    "🧍",
-    "🧎",
-    "🏃",
-    "💃",
-    "🕺",
-    "🕴",
-    "👯",
-    "🧖",
-    "🧗",
-    "🤺",
-    "🏇",
-    "⛷",
-    "🏂",
-    "🏌",
-    "🏄",
-    "🚣",
-    "🏊",
-    "⛹",
-    "🏋",
-    "🚴",
-    "🚵",
-    "🤸",
-    "🤼",
-    "🤽",
-    "🤾",
-    "🤹",
-    "🧘",
-    "🛀",
-    "🛌",
-    "👭",
-    "👫",
-    "👬",
-    "💏",
-    "💑",
-    "🗣",
-    "👤",
-    "👥",
-    "🫂",
-    "👪",
-    "👣",
-    "🐵",
-    "🐒",
-    "🦍",
-    "🦧",
-    "🐶",
-    "🐕",
-    "🦮",
-    "🐩",
-    "🐺",
-    "🦊",
-    "🦝",
-    "🐱",
-    "🐈",
-    "🦁",
-    "🐯",
-    "🐅",
-    "🐆",
-    "🐴",
-    "🫎",
-    "🫏",
-    "🐎",
-    "🦄",
-    "🦓",
-    "🦌",
-    "🦬",
-    "🐮",
-    "🐂",
-    "🐃",
-    "🐄",
-    "🐷",
-    "🐖",
-    "🐗",
-    "🐽",
-    "🐏",
-    "🐑",
-    "🐐",
-    "🐪",
-    "🐫",
-    "🦙",
-    "🦒",
-    "🐘",
-    "🦣",
-    "🦏",
-    "🦛",
-    "🐭",
-    "🐁",
-    "🐀",
-    "🐹",
-    "🐰",
-    "🐇",
-    "🐿",
-    "🦫",
-    "🦔",
-    "🦇",
-    "🐻",
-    "🐨",
-    "🐼",
-    "🦥",
-    "🦦",
-    "🦨",
-    "🦘",
-    "🦡",
-    "🐾",
-    "🦃",
-    "🐔",
-    "🐓",
-    "🐣",
-    "🐤",
-    "🐥",
-    "🐦",
-    "🐧",
-    "🕊",
-    "🦅",
-    "🦆",
-    "🦢",
-    "🦉",
-    "🦤",
-    "🪶",
-    "🦩",
-    "🦚",
-    "🦜",
-    "🪽",
-    "🪿",
-    "🐸",
-    "🐊",
-    "🐢",
-    "🦎",
-    "🐍",
-    "🐲",
-    "🐉",
-    "🦕",
-    "🦖",
-    "🐳",
-    "🐋",
-    "🐬",
-    "🦭",
-    "🐟",
-    "🐠",
-    "🐡",
-    "🦈",
-    "🐙",
-    "🐚",
-    "🪸",
-    "🪼",
-    "🦀",
-    "🦞",
-    "🦐",
-    "🦑",
-    "🦪",
-    "🐌",
-    "🦋",
-    "🐛",
-    "🐜",
-    "🐝",
-    "🪲",
-    "🐞",
-    "🦗",
-    "🪳",
-    "🕷",
-    "🕸",
-    "🦂",
-    "🦟",
-    "🪰",
-    "🪱",
-    "🦠",
-    "💐",
-    "🌸",
-    "💮",
-    "🪷",
-    "🏵",
-    "🌹",
-    "🥀",
-    "🌺",
-    "🌻",
-    "🌼",
-    "🌷",
-    "🪻",
-    "🌱",
-    "🪴",
-    "🌲",
-    "🌳",
-    "🌴",
-    "🌵",
-    "🌾",
-    "🌿",
-    "☘",
-    "🍀",
-    "🍁",
-    "🍂",
-    "🍃",
-    "🪹",
-    "🪺",
-    "🍄",
-    "🍇",
-    "🍈",
-    "🍉",
-    "🍊",
-    "🍋",
-    "🍌",
-    "🍍",
-    "🥭",
-    "🍎",
-    "🍏",
-    "🍐",
-    "🍑",
-    "🍒",
-    "🍓",
-    "🫐",
-    "🥝",
-    "🍅",
-    "🫒",
-    "🥥",
-    "🥑",
-    "🍆",
-    "🥔",
-    "🥕",
-    "🌽",
-    "🌶",
-    "🫑",
-    "🥒",
-    "🥬",
-    "🥦",
-    "🧄",
-    "🧅",
-    "🥜",
-    "🫘",
-    "🌰",
-    "🫚",
-    "🫛",
-    "🍞",
-    "🥐",
-    "🥖",
-    "🫓",
-    "🥨",
-    "🥯",
-    "🥞",
-    "🧇",
-    "🧀",
-    "🍖",
-    "🍗",
-    "🥩",
-    "🥓",
-    "🍔",
-    "🍟",
-    "🍕",
-    "🌭",
-    "🥪",
-    "🌮",
-    "🌯",
-    "🫔",
-    "🥙",
-    "🧆",
-    "🥚",
-    "🍳",
-    "🥘",
-    "🍲",
-    "🫕",
-    "🥣",
-    "🥗",
-    "🍿",
-    "🧈",
-    "🧂",
-    "🥫",
-    "🍱",
-    "🍘",
-    "🍙",
-    "🍚",
-    "🍛",
-    "🍜",
-    "🍝",
-    "🍠",
-    "🍢",
-    "🍣",
-    "🍤",
-    "🍥",
-    "🥮",
-    "🍡",
-    "🥟",
-    "🥠",
-    "🥡",
-    "🍦",
-    "🍧",
-    "🍨",
-    "🍩",
-    "🍪",
-    "🎂",
-    "🍰",
-    "🧁",
-    "🥧",
-    "🍫",
-    "🍬",
-    "🍭",
-    "🍮",
-    "🍯",
-    "🍼",
-    "🥛",
-    "☕",
-    "🫖",
-    "🍵",
-    "🍶",
-    "\u{01f3f3}\u{fe0f}\u{200d}\u{26a7}\u{fe0f}",
-    "\u{01f44d}\u{01f3fe}",
-];
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2024 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use std::{
+    fs::File,
+    io::{BufRead, BufReader},
+    path::{Path, PathBuf},
+    sync::{Arc, LazyLock, Mutex as SyncMutex},
+};
+
+use crate::{
+    gfx::{
+        GfxDrawCall, GfxDrawInstruction, GfxDrawMesh, GfxTextureId, ManagedTexturePtr, Point,
+        Rectangle, RenderApi,
+    },
+    mesh::{MeshBuilder, MeshInfo, COLOR_WHITE},
+    prop::{
+        PropertyAtomicGuard, PropertyFloat32, PropertyPtr, PropertyRect, PropertyStr,
+        PropertyUint32, Role,
+    },
+    scene::{Pimpl, SceneNodePtr, SceneNodeWeak},
+    text::{self, GlyphPositionIter, TextShaper, TextShaperPtr},
+    util::unixtime,
+    ExecutorPtr,
+};
+
+use super::default;
+
+#[cfg(target_os = "android")]
+pub fn get_emoji_list_path() -> PathBuf {
+    get_external_storage_path().join("emoji.txt")
+}
+
+#[cfg(not(target_os = "android"))]
+pub fn get_emoji_list_path() -> PathBuf {
+    dirs::data_local_dir().unwrap().join("darkfi/emoji.txt")
+}
+
+pub type EmojiMeshesPtr = Arc<SyncMutex<EmojiMeshes>>;
+
+pub struct EmojiMeshes {
+    render_api: RenderApi,
+    text_shaper: TextShaperPtr,
+    emoji_size: f32,
+    emoji_list: LazyLock<Vec<String>>,
+    meshes: Vec<GfxDrawMesh>,
+}
+
+impl EmojiMeshes {
+    pub fn new(
+        render_api: RenderApi,
+        text_shaper: TextShaperPtr,
+        emoji_size: f32,
+    ) -> EmojiMeshesPtr {
+        Arc::new(SyncMutex::new(Self {
+            render_api,
+            text_shaper,
+            emoji_size,
+            emoji_list: LazyLock::new(load_emoji_list),
+            meshes: vec![],
+        }))
+    }
+
+    pub fn get(&mut self, i: usize) -> GfxDrawMesh {
+        let emoji_list = self.get_list();
+        assert!(i < emoji_list.len());
+        self.meshes.reserve_exact(emoji_list.len());
+
+        if i >= self.meshes.len() {
+            //d!("EmojiMeshes loading new glyphs");
+            for j in self.meshes.len()..=i {
+                let emoji = &self.emoji_list[j];
+                let mesh = self.gen_emoji_mesh(emoji);
+                self.meshes.push(mesh);
+            }
+        }
+
+        self.meshes[i].clone()
+    }
+
+    /// Make mesh for this emoji centered at (0, 0)
+    fn gen_emoji_mesh(&self, emoji: &str) -> GfxDrawMesh {
+        //d!("rendering emoji: '{emoji}'");
+        // The params here don't actually matter since we're talking about BMP fixed sizes
+        let glyphs = self.text_shaper.shape(emoji.to_string(), 10., 1.);
+        assert_eq!(glyphs.len(), 1);
+        let atlas = text::make_texture_atlas(&self.render_api, &glyphs);
+        let glyph = glyphs.into_iter().next().unwrap();
+
+        // Emoji's vary in size. We make them all a consistent size.
+        let w = self.emoji_size;
+        let h =
+            (glyph.sprite.bmp_height as f32) * self.emoji_size / (glyph.sprite.bmp_width as f32);
+        // Center at origin
+        let x = -w / 2.;
+        let y = -h / 2.;
+
+        let uv = atlas.fetch_uv(glyph.glyph_id).expect("missing glyph UV rect");
+        let mut mesh = MeshBuilder::new();
+        mesh.draw_box(&Rectangle::new(x, y, w, h), COLOR_WHITE, &uv);
+        mesh.alloc(&self.render_api).draw_with_texture(atlas.texture)
+    }
+
+    pub fn get_list<'a>(&'a self) -> &'a Vec<String> {
+        LazyLock::force(&self.emoji_list)
+    }
+}
+
+fn load_emoji_list() -> Vec<String> {
+    match load_custom_emoji_list(&get_emoji_list_path()) {
+        Some(emojis) => emojis,
+        None => default::create_default_emoji_list(),
+    }
+}
+
+fn load_custom_emoji_list(path: &Path) -> Option<Vec<String>> {
+    let file = File::open(path).ok()?;
+    let reader = BufReader::new(file);
+
+    let mut emojis = vec![];
+    for mut line in reader.lines().map_while(Result::ok) {
+        remove_whitespace(&mut line);
+        if line.starts_with('#') {
+            continue
+        }
+        let emoji = unescape_unicode(&line)?;
+        emojis.push(emoji);
+    }
+    Some(emojis)
+}
+
+fn unescape_unicode(input: &str) -> Option<String> {
+    let re = regex::Regex::new(r"\\u\{([0-9A-Fa-f]+)\}").unwrap();
+
+    // There is no way to bail from a failed regex so we use this workaround instead.
+    let mut failed = false;
+    let result = re
+        .replace_all(input, |caps: &regex::Captures| {
+            let Ok(code) = u32::from_str_radix(&caps[1], 16) else {
+                failed = true;
+                return String::new();
+            };
+            let Some(chr) = char::from_u32(code) else {
+                failed = true;
+                return String::new();
+            };
+            chr.to_string()
+        })
+        .into_owned();
+
+    if failed {
+        return None
+    }
+
+    Some(result)
+}
+
+fn remove_whitespace(s: &mut String) {
+    s.retain(|c| !c.is_whitespace());
+}

+ 27 - 75
bin/darkwallet/src/ui/emoji_picker/mod.rs

@@ -47,73 +47,13 @@ use crate::{
 
 use super::{DrawUpdate, OnModify, UIObject};
 
+mod default;
 mod emoji;
+pub use emoji::{EmojiMeshes, EmojiMeshesPtr};
 
 macro_rules! d { ($($arg:tt)*) => { debug!(target: "ui::emoji_picker", $($arg)*); } }
 macro_rules! t { ($($arg:tt)*) => { trace!(target: "ui::emoji_picker", $($arg)*); } }
 
-pub type EmojiMeshesPtr = Arc<SyncMutex<EmojiMeshes>>;
-
-pub struct EmojiMeshes {
-    render_api: RenderApi,
-    text_shaper: TextShaperPtr,
-    emoji_size: f32,
-    meshes: Vec<GfxDrawMesh>,
-}
-
-impl EmojiMeshes {
-    pub fn new(
-        render_api: RenderApi,
-        text_shaper: TextShaperPtr,
-        emoji_size: f32,
-    ) -> EmojiMeshesPtr {
-        Arc::new(SyncMutex::new(Self {
-            render_api,
-            text_shaper,
-            emoji_size,
-            meshes: Vec::with_capacity(emoji::EMOJI_LIST.len()),
-        }))
-    }
-
-    /// Make mesh for this emoji centered at (0, 0)
-    fn gen_emoji_mesh(&self, emoji: &str) -> GfxDrawMesh {
-        //d!("rendering emoji: '{emoji}'");
-        // The params here don't actually matter since we're talking about BMP fixed sizes
-        let glyphs = self.text_shaper.shape(emoji.to_string(), 10., 1.);
-        assert_eq!(glyphs.len(), 1);
-        let atlas = text::make_texture_atlas(&self.render_api, &glyphs);
-        let glyph = glyphs.into_iter().next().unwrap();
-
-        // Emoji's vary in size. We make them all a consistent size.
-        let w = self.emoji_size;
-        let h =
-            (glyph.sprite.bmp_height as f32) * self.emoji_size / (glyph.sprite.bmp_width as f32);
-        // Center at origin
-        let x = -w / 2.;
-        let y = -h / 2.;
-
-        let uv = atlas.fetch_uv(glyph.glyph_id).expect("missing glyph UV rect");
-        let mut mesh = MeshBuilder::new();
-        mesh.draw_box(&Rectangle::new(x, y, w, h), COLOR_WHITE, &uv);
-        mesh.alloc(&self.render_api).draw_with_texture(atlas.texture)
-    }
-
-    pub fn get(&mut self, i: usize) -> GfxDrawMesh {
-        assert!(i < emoji::EMOJI_LIST.len());
-
-        if i >= self.meshes.len() {
-            //d!("EmojiMeshes loading new glyphs");
-            for j in self.meshes.len()..=i {
-                let emoji = emoji::EMOJI_LIST[j];
-                let mesh = self.gen_emoji_mesh(emoji);
-                self.meshes.push(mesh);
-            }
-        }
-
-        self.meshes[i].clone()
-    }
-}
-
 struct TouchInfo {
     start_pos: Point,
     start_scroll: f32,
@@ -204,7 +144,7 @@ impl EmojiPicker {
     }
 
     fn max_scroll(&self) -> f32 {
-        let emojis_len = emoji::EMOJI_LIST.len() as f32;
+        let emojis_len = self.emoji_meshes.lock().unwrap().get_list().len() as f32;
         let emoji_size = self.emoji_size.get();
         let cols = self.emojis_per_line();
         let rows = (emojis_len / cols).ceil();
@@ -237,15 +177,26 @@ impl EmojiPicker {
         let idx = (col + row * n_cols).round() as usize;
         //d!("    = {idx}, emoji_len = {}", emoji::EMOJI_LIST.len());
 
-        if idx < emoji::EMOJI_LIST.len() {
-            let emoji = emoji::EMOJI_LIST[idx];
-            d!("Selected emoji: {emoji}");
-            let mut param_data = vec![];
-            emoji.encode(&mut param_data).unwrap();
-            let node = self.node.upgrade().unwrap();
-            node.trigger("emoji_select", param_data).await.unwrap();
-        } else {
-            d!("Index out of bounds: {idx}");
+        let emoji_selected = {
+            let mut emoji_meshes = self.emoji_meshes.lock().unwrap();
+            let emoji_list = emoji_meshes.get_list();
+
+            if idx < emoji_list.len() {
+                let emoji = emoji_list[idx].clone();
+                Some(emoji)
+            } else {
+                None
+            }
+        };
+        match emoji_selected {
+            Some(emoji) => {
+                d!("Selected emoji: {emoji}");
+                let mut param_data = vec![];
+                emoji.encode(&mut param_data).unwrap();
+                let node = self.node.upgrade().unwrap();
+                node.trigger("emoji_select", param_data).await.unwrap();
+            }
+            None => d!("Index out of bounds: {idx}"),
         }
     }
 
@@ -253,6 +204,7 @@ impl EmojiPicker {
         let atom = &mut PropertyAtomicGuard::new();
         let trace_id = rand::random();
         let timest = unixtime();
+        t!("redraw({:?}) [timest={timest}, trace_id={trace_id}]", self.node.upgrade().unwrap());
         let Some(parent_rect) = self.parent_rect.lock().unwrap().clone() else { return };
 
         let Some(draw_update) = self.get_draw_calls(parent_rect, trace_id, atom) else {
@@ -260,7 +212,7 @@ impl EmojiPicker {
             return;
         };
         self.render_api.replace_draw_calls(timest, draw_update.draw_calls);
-        t!("replace draw calls done");
+        t!("redraw DONE [trace_id={trace_id}]");
     }
 
     fn get_draw_calls(
@@ -287,10 +239,11 @@ impl EmojiPicker {
         let emoji_size = self.emoji_size.get();
 
         let mut emoji_meshes = self.emoji_meshes.lock().unwrap();
+        let emoji_list_len = emoji_meshes.get_list().len();
 
         let mut x = emoji_size / 2.;
         let mut y = emoji_size / 2. - self.scroll.get();
-        for (i, mesh) in emoji::EMOJI_LIST.iter().enumerate() {
+        for i in 0..emoji_list_len {
             let pos = Point::new(x, y);
             let mesh = emoji_meshes.get(i);
             instrs.extend_from_slice(&[
@@ -347,7 +300,6 @@ impl UIObject for EmojiPicker {
         atom: &mut PropertyAtomicGuard,
     ) -> Option<DrawUpdate> {
         t!("EmojiPicker::draw({parent_rect:?}, {trace_id})");
-
         *self.parent_rect.lock().unwrap() = Some(parent_rect);
         self.get_draw_calls(parent_rect, trace_id, atom)
     }

+ 8 - 8
bin/darkwallet/src/ui/shortcut.rs

@@ -223,14 +223,14 @@ fn keycode_to_strs(key: KeyCode, mods: KeyMods) -> Vec<String> {
         KeyCode::KpAdd => keys.push("kpadd"),
         KeyCode::KpEnter => keys.push("kpenter"),
         KeyCode::KpEqual => keys.push("kpequal"),
-        KeyCode::LeftShift => keys.push("leftshift"),
-        KeyCode::LeftControl => keys.push("leftcontrol"),
-        KeyCode::LeftAlt => keys.push("leftalt"),
-        KeyCode::LeftSuper => keys.push("leftsuper"),
-        KeyCode::RightShift => keys.push("rightshift"),
-        KeyCode::RightControl => keys.push("rightcontrol"),
-        KeyCode::RightAlt => keys.push("rightalt"),
-        KeyCode::RightSuper => keys.push("rightsuper"),
+        KeyCode::LeftShift |
+        KeyCode::LeftControl |
+        KeyCode::LeftAlt |
+        KeyCode::LeftSuper |
+        KeyCode::RightShift |
+        KeyCode::RightControl |
+        KeyCode::RightAlt |
+        KeyCode::RightSuper => {}
         KeyCode::Menu => keys.push("menu"),
         KeyCode::Back => keys.push("back"),
         KeyCode::Unknown => {