diff --git a/Cargo.lock b/Cargo.lock index 0c20ab12..f61fef15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -191,6 +191,28 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "alsa" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +dependencies = [ + "alsa-sys", + "bitflags 2.13.2", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "android_system_properties" version = "0.1.6" @@ -600,7 +622,7 @@ dependencies = [ "anyhow", "arrayvec", "log", - "nom", + "nom 8.0.0", "num-rational", "v_frame", ] @@ -676,6 +698,24 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efbd3e1070bbdf4cd88a75264e18e8a26f7cb5c6949eadf0ceb85fb159cf08f8" +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.13.2", + "cexpr", + "clang-sys", + "itertools 0.11.0", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.3", + "shlex 1.3.0", + "syn 2.0.119", +] + [[package]] name = "bip39" version = "2.2.2" @@ -1001,7 +1041,22 @@ dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex", + "shlex 2.0.1", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", ] [[package]] @@ -1130,6 +1185,17 @@ dependencies = [ "inout 0.2.5", ] +[[package]] +name = "clang-sys" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "cmake" version = "0.1.58" @@ -1521,6 +1587,26 @@ dependencies = [ "libm", ] +[[package]] +name = "coreaudio-rs" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +dependencies = [ + "bitflags 1.3.2", + "core-foundation-sys", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9b4739a805a62757a83e5654fa3faabec0442666b263bb2287d5a8185bfd953" +dependencies = [ + "bindgen", +] + [[package]] name = "cosmic-text" version = "0.19.0" @@ -1545,6 +1631,29 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cpal" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +dependencies = [ + "alsa", + "core-foundation-sys", + "coreaudio-rs", + "dasp_sample", + "jni 0.21.1", + "js-sys", + "libc", + "mach2 0.4.3", + "ndk", + "ndk-context", + "oboe", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.54.0", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1694,6 +1803,12 @@ dependencies = [ "syn 3.0.9", ] +[[package]] +name = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + [[package]] name = "data-encoding" version = "2.11.1" @@ -2774,8 +2889,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "465a363235cd4062ef5f0953fc096fc764ed6f53dfbd9f409f669f93ea3b1ff9" dependencies = [ "anyhow", + "block2 0.6.2", "chrono", "core-text", + "cpal", "enum-iterator", "gpui-base", "gpui-component-macros", @@ -2792,7 +2909,10 @@ dependencies = [ "num-traits", "objc2 0.6.5", "objc2-app-kit 0.3.2", + "objc2-av-foundation", + "objc2-avf-audio", "objc2-foundation 0.3.2", + "objc2-speech", "once_cell", "paste", "raw-window-handle", @@ -2946,7 +3066,7 @@ dependencies = [ "image", "libc", "log", - "mach2", + "mach2 0.6.0", "metal", "objc2 0.6.5", "objc2-core-foundation", @@ -4206,6 +4326,22 @@ dependencies = [ "jiff-tzdb", ] +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + [[package]] name = "jni" version = "0.22.4" @@ -4580,6 +4716,15 @@ dependencies = [ "uuid", ] +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + [[package]] name = "mach2" version = "0.6.0" @@ -4802,6 +4947,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "minisign-verify" version = "0.2.5" @@ -4935,12 +5086,35 @@ dependencies = [ "native-theme", ] +[[package]] +name = "ndk" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" +dependencies = [ + "bitflags 2.13.2", + "jni-sys 0.3.1", + "log", + "ndk-sys 0.5.0+25.2.9519653", + "num_enum", + "thiserror 1.0.69", +] + [[package]] name = "ndk-context" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys 0.3.1", +] + [[package]] name = "ndk-sys" version = "0.6.0+11769913" @@ -5034,6 +5208,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nom" version = "8.0.0" @@ -5392,6 +5576,28 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "objc" version = "0.2.7" @@ -5475,6 +5681,27 @@ dependencies = [ "objc2-quartz-core 0.3.2", ] +[[package]] +name = "objc2-av-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478ae33fcac9df0a18db8302387c666b8ef08a3e2d62b510ca4fc278a384b6c0" +dependencies = [ + "bitflags 2.13.2", + "objc2 0.6.5", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-avf-audio" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" +dependencies = [ + "objc2 0.6.5", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-cloud-kit" version = "0.3.2" @@ -5696,6 +5923,18 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-speech" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9eb609f9d2a25e0f8b76954f3acaa58348ce2a91285fe867643b2224ac4d263" +dependencies = [ + "block2 0.6.2", + "objc2 0.6.5", + "objc2-avf-audio", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-user-notifications" version = "0.3.2" @@ -5735,6 +5974,29 @@ dependencies = [ "memchr", ] +[[package]] +name = "oboe" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" +dependencies = [ + "jni 0.21.1", + "ndk", + "ndk-context", + "num-derive", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" +dependencies = [ + "cc", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -7024,7 +7286,7 @@ checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", - "jni", + "jni 0.22.4", "log", "once_cell", "rustls", @@ -7516,6 +7778,12 @@ dependencies = [ "dirs 6.0.0", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "shlex" version = "2.0.1" @@ -9109,7 +9377,7 @@ version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2c8ddfbbf66e501434657400268cefe51b95140a0266236415cda7627732df7" dependencies = [ - "jni", + "jni 0.22.4", "log", "ndk-context", "objc2 0.6.5", @@ -9278,7 +9546,7 @@ dependencies = [ "libloading", "log", "naga", - "ndk-sys", + "ndk-sys 0.6.0+11769913", "objc2 0.6.5", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -9370,6 +9638,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core 0.54.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.57.0" @@ -9446,6 +9724,16 @@ dependencies = [ "windows-core 0.62.2", ] +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.57.0" @@ -9712,6 +10000,15 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -9739,6 +10036,21 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -9814,6 +10126,12 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -9832,6 +10150,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -9850,6 +10174,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -9880,6 +10210,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -9898,6 +10234,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -9916,6 +10258,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -9934,6 +10282,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" diff --git a/crates/chat_ui/Cargo.toml b/crates/chat_ui/Cargo.toml index 810ba59e..7c8b97c4 100644 --- a/crates/chat_ui/Cargo.toml +++ b/crates/chat_ui/Cargo.toml @@ -7,7 +7,7 @@ publish.workspace = true [dependencies] state = { path = "../state" } ui = { path = "../ui" } -gpui-kit.workspace = true +gpui-kit = { workspace = true, features = ["speech"] } common = { path = "../common" } person = { path = "../person" } chat = { path = "../chat" } diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index e339002b..4a338f32 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -20,6 +20,7 @@ use gpui_kit::component::input::{Input, InputEvent, InputState, Textarea, Textar use gpui_kit::component::menu::DropdownMenu; use gpui_kit::component::notification::Notification; use gpui_kit::component::scroll::Scrollbar; +use gpui_kit::component::speech::{SpeechButton, SpeechEvent, SpeechState, SpeechWaveform}; use gpui_kit::component::tooltip::Tooltip; use gpui_kit::component::{ActiveTheme, Disableable, Icon, Sizable, WindowExt, h_flex, v_flex}; use itertools::Itertools; @@ -91,6 +92,9 @@ pub struct ChatPanel { /// Chat input state input: Entity, + /// Dictation session for the chat input + speech: Entity, + /// Subject input state subject_input: Entity, @@ -116,7 +120,7 @@ pub struct ChatPanel { tasks: Vec>>, /// Event subscriptions - subscriptions: SmallVec<[Subscription; 3]>, + subscriptions: SmallVec<[Subscription; 4]>, } impl ChatPanel { @@ -160,6 +164,9 @@ impl ChatPanel { .clean_on_escape() }); + // Define dictation state + let speech = cx.new(SpeechState::new); + // Define subject input state let subject_input = cx.new(|cx| InputState::new(window, cx).placeholder("New subject...")); let subject_bar = cx.new(|_cx| false); @@ -178,15 +185,28 @@ impl ChatPanel { subscriptions.push( // Subscribe the subject input event - cx.subscribe_in( - &subject_input, - window, - move |this, _input, event, window, cx| { - if let InputEvent::PressEnter { .. } = event { - this.change_subject(window, cx); - }; - }, - ), + cx.subscribe_in(&subject_input, window, move |this, _, event, window, cx| { + if let InputEvent::PressEnter { .. } = event { + this.change_subject(window, cx); + }; + }), + ); + + subscriptions.push( + // Insert the dictated text into the chat input + cx.subscribe_in(&speech, window, move |this, _speech, event, window, cx| { + match event { + SpeechEvent::Final(text) if !text.is_empty() => { + this.input.update(cx, |input, cx| { + input.insert(text.clone(), window, cx); + }); + } + SpeechEvent::Error(error) => { + window.push_notification(Notification::error(error.to_string()), cx); + } + _ => {} + }; + }), ); // Define all functions that will run after the current cycle @@ -206,6 +226,7 @@ impl ChatPanel { directory, list_state, input, + speech, subject_input, subject_bar, replies_to, @@ -1807,6 +1828,7 @@ impl Render for ChatPanel { let is_typing = !self.input.read(cx).value().trim().is_empty(); let pending_attachments = !self.encrypted_attachments.read(cx).is_empty(); + let capturing = self.speech.read(cx).status().is_capturing(); v_flex() .image_cache(retain_all(self.id.clone())) @@ -1901,6 +1923,18 @@ impl Render for ChatPanel { .py_1() .flex_1(), ) + .child( + h_flex() + .items_end() + .gap_1() + .pl_1() + .when(capturing, |this| { + this.child( + SpeechWaveform::new(&self.speech).xsmall().w_12(), + ) + }) + .child(SpeechButton::new(&self.speech)), + ) .child( h_flex() .pl_1() diff --git a/crates/nip29_ui/Cargo.toml b/crates/nip29_ui/Cargo.toml index 7827e6ce..0d0783fc 100644 --- a/crates/nip29_ui/Cargo.toml +++ b/crates/nip29_ui/Cargo.toml @@ -12,7 +12,7 @@ nip29 = { path = "../nip29" } person = { path = "../person" } settings = { path = "../settings" } state = { path = "../state" } -gpui-kit.workspace = true +gpui-kit = { workspace = true, features = ["speech"] } ui = { path = "../ui" } gpui.workspace = true diff --git a/crates/nip29_ui/src/lib.rs b/crates/nip29_ui/src/lib.rs index cb2dbbd4..c89cd8e5 100644 --- a/crates/nip29_ui/src/lib.rs +++ b/crates/nip29_ui/src/lib.rs @@ -15,6 +15,7 @@ use gpui_kit::component::dock::{Panel, PanelEvent}; use gpui_kit::component::input::{InputEvent, Textarea, TextareaState}; use gpui_kit::component::notification::Notification; use gpui_kit::component::scroll::Scrollbar; +use gpui_kit::component::speech::{SpeechButton, SpeechEvent, SpeechState, SpeechWaveform}; use gpui_kit::component::{ActiveTheme, Disableable, Icon, Sizable, WindowExt, h_flex, v_flex}; use nip29::{Group, GroupEvent, GroupKey, GroupsRegistry, Membership, Pin, Row}; use nostr_sdk::prelude::*; @@ -312,6 +313,8 @@ pub struct GroupPanel { reply_to: Option, rendered_texts_by_id: BTreeMap, input: Entity, + /// Dictation session for the composer input + speech: Entity, /// Uploaded, non-encrypted attachments waiting to be sent. attachments: Vec, /// Whether an upload is in flight. @@ -319,7 +322,7 @@ pub struct GroupPanel { /// Whether the user closed the encryption notice this visit notice_dismissed: bool, tasks: SmallVec<[Task>; 4]>, - _subscriptions: SmallVec<[Subscription; 3]>, + _subscriptions: SmallVec<[Subscription; 4]>, } impl GroupPanel { @@ -338,6 +341,8 @@ impl GroupPanel { .clean_on_escape() }); + let speech = cx.new(SpeechState::new); + let mut subscriptions = smallvec![]; subscriptions.push( @@ -348,6 +353,22 @@ impl GroupPanel { }), ); + subscriptions.push( + cx.subscribe_in(&speech, window, |this, _, event, window, cx| { + match event { + SpeechEvent::Final(text) if !text.is_empty() => { + this.input.update(cx, |input, cx| { + input.insert(text.clone(), window, cx); + }); + } + SpeechEvent::Error(error) => { + window.push_notification(Notification::error(error.to_string()), cx); + } + _ => {} + }; + }), + ); + subscriptions.push(cx.subscribe(&group, |this, _group, event, cx| { if !matches!(event, GroupEvent::Sent(_)) { this.sync(cx); @@ -383,6 +404,7 @@ impl GroupPanel { notice_dismissed: false, rendered_texts_by_id: BTreeMap::new(), input, + speech, attachments: Vec::new(), uploading: false, tasks: smallvec![], @@ -1073,6 +1095,10 @@ impl GroupPanel { .py_1() .flex_1(), ) + .when(self.speech.read(cx).status().is_capturing(), |this| { + this.child(SpeechWaveform::new(&self.speech).xsmall().w_12()) + }) + .child(SpeechButton::new(&self.speech)) .child( Button::new("send") .icon(ui::IconName::PaperPlaneFill) diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index adf1d030..781e8b11 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -26,6 +26,11 @@ icons = [ "resources/icon.ico", ] +# Merged over the generated Info.plist; required for the speech input to +# request microphone and speech recognition access on macOS. +[package.metadata.packager.macos] +info_plist_path = "resources/Info.plist" + [dependencies] assets = { path = "../crates/assets" } workspace = { path = "../crates/workspace" } diff --git a/desktop/resources/Info.plist b/desktop/resources/Info.plist new file mode 100644 index 00000000..c0bfc62a --- /dev/null +++ b/desktop/resources/Info.plist @@ -0,0 +1,10 @@ + + + + + NSMicrophoneUsageDescription + Dictate text into messages. + NSSpeechRecognitionUsageDescription + Turn your speech into text. + +