Compare commits
26 Commits
github-tok
...
v0.120.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6836cbc91 | ||
|
|
58e824d98a | ||
|
|
94e23558a3 | ||
|
|
119fa041bc | ||
|
|
ae34c15bf6 | ||
|
|
ef5fc9a33c | ||
|
|
c876f81591 | ||
|
|
f9fb4eecf7 | ||
|
|
35d1876794 | ||
|
|
8899c4d840 | ||
|
|
965d68548f | ||
|
|
3b70171411 | ||
|
|
9b79f04463 | ||
|
|
6c69f4e5c6 | ||
|
|
4ad3cf71b7 | ||
|
|
a15b307a17 | ||
|
|
27320f0238 | ||
|
|
09331d6a90 | ||
|
|
8650a3da7a | ||
|
|
dc9f3af023 | ||
|
|
9c62880b5f | ||
|
|
80f9dc34f0 | ||
|
|
e8873b5fc8 | ||
|
|
ffbd5a4bfb | ||
|
|
20d35fdf78 | ||
|
|
ddc437e92c |
245
Cargo.lock
generated
245
Cargo.lock
generated
@@ -101,50 +101,25 @@ dependencies = [
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alacritty_config"
|
||||
version = "0.1.2-dev"
|
||||
source = "git+https://github.com/zed-industries/alacritty?rev=33306142195b354ef3485ca2b1d8a85dfc6605ca#33306142195b354ef3485ca2b1d8a85dfc6605ca"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
"toml 0.7.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alacritty_config_derive"
|
||||
version = "0.2.2-dev"
|
||||
source = "git+https://github.com/zed-industries/alacritty?rev=33306142195b354ef3485ca2b1d8a85dfc6605ca#33306142195b354ef3485ca2b1d8a85dfc6605ca"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alacritty_terminal"
|
||||
version = "0.20.0-dev"
|
||||
source = "git+https://github.com/zed-industries/alacritty?rev=33306142195b354ef3485ca2b1d8a85dfc6605ca#33306142195b354ef3485ca2b1d8a85dfc6605ca"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35229555d7cc7e83392dfc27c96bec560b1076d756184893296cd60125f4a264"
|
||||
dependencies = [
|
||||
"alacritty_config",
|
||||
"alacritty_config_derive",
|
||||
"base64 0.13.1",
|
||||
"base64 0.21.4",
|
||||
"bitflags 2.4.1",
|
||||
"home",
|
||||
"libc",
|
||||
"log",
|
||||
"mio 0.6.23",
|
||||
"mio-anonymous-pipes",
|
||||
"mio-extras",
|
||||
"miow 0.3.7",
|
||||
"nix 0.26.4",
|
||||
"miow 0.6.0",
|
||||
"parking_lot 0.12.1",
|
||||
"regex-automata 0.1.10",
|
||||
"piper",
|
||||
"polling 3.3.2",
|
||||
"regex-automata 0.4.5",
|
||||
"rustix-openpty",
|
||||
"serde",
|
||||
"serde_yaml",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"toml 0.7.8",
|
||||
"unicode-width",
|
||||
"vte",
|
||||
"windows-sys 0.48.0",
|
||||
@@ -444,7 +419,7 @@ dependencies = [
|
||||
"futures-lite",
|
||||
"log",
|
||||
"parking",
|
||||
"polling",
|
||||
"polling 2.8.0",
|
||||
"rustix 0.37.23",
|
||||
"slab",
|
||||
"socket2 0.4.9",
|
||||
@@ -1155,7 +1130,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"syn 1.0.109",
|
||||
"tempfile",
|
||||
"toml 0.5.11",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1452,7 +1427,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.40.1"
|
||||
version = "0.40.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -1514,7 +1489,7 @@ dependencies = [
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"toml 0.5.11",
|
||||
"toml",
|
||||
"tonic",
|
||||
"tower",
|
||||
"tracing",
|
||||
@@ -1599,7 +1574,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"story",
|
||||
"toml 0.5.11",
|
||||
"toml",
|
||||
"util",
|
||||
"uuid 1.4.1",
|
||||
]
|
||||
@@ -2014,6 +1989,12 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cursor-icon"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991"
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "5.5.3"
|
||||
@@ -3597,7 +3578,7 @@ dependencies = [
|
||||
"log",
|
||||
"mime",
|
||||
"once_cell",
|
||||
"polling",
|
||||
"polling 2.8.0",
|
||||
"slab",
|
||||
"sluice",
|
||||
"tracing",
|
||||
@@ -3937,12 +3918,6 @@ dependencies = [
|
||||
"safemem",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||
|
||||
[[package]]
|
||||
name = "linkme"
|
||||
version = "0.3.17"
|
||||
@@ -4299,19 +4274,6 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio-anonymous-pipes"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6bc513025fe5005a3aa561b50fdb2cda5a150b84800ae02acd8aa9ed62ca1a6b"
|
||||
dependencies = [
|
||||
"mio 0.6.23",
|
||||
"miow 0.3.7",
|
||||
"parking_lot 0.11.2",
|
||||
"spsc-buffer",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio-extras"
|
||||
version = "2.0.6"
|
||||
@@ -4324,17 +4286,6 @@ dependencies = [
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio-uds"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0"
|
||||
dependencies = [
|
||||
"iovec",
|
||||
"libc",
|
||||
"mio 0.6.23",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miow"
|
||||
version = "0.2.2"
|
||||
@@ -4349,11 +4300,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "miow"
|
||||
version = "0.3.7"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
|
||||
checksum = "359f76430b20a79f9e20e115b3428614e654f04fab314482fc0fda0ebd3c6044"
|
||||
dependencies = [
|
||||
"winapi 0.3.9",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4513,17 +4464,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.27.1"
|
||||
@@ -5294,6 +5234,17 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "piper"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"fastrand 2.0.0",
|
||||
"futures-io",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkcs1"
|
||||
version = "0.7.5"
|
||||
@@ -5385,6 +5336,20 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
version = "3.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "545c980a3880efd47b2e262f6a4bb6daad6555cf3367aa9c4e52895f69537a41"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"concurrent-queue",
|
||||
"pin-project-lite 0.2.13",
|
||||
"rustix 0.38.21",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pollster"
|
||||
version = "0.2.5"
|
||||
@@ -5461,7 +5426,7 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785"
|
||||
dependencies = [
|
||||
"toml 0.5.11",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5568,7 +5533,7 @@ dependencies = [
|
||||
"terminal",
|
||||
"text",
|
||||
"thiserror",
|
||||
"toml 0.5.11",
|
||||
"toml",
|
||||
"unindent",
|
||||
"util",
|
||||
]
|
||||
@@ -6039,6 +6004,17 @@ dependencies = [
|
||||
"regex-syntax 0.7.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.29"
|
||||
@@ -6051,6 +6027,12 @@ version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
||||
|
||||
[[package]]
|
||||
name = "remove_dir_all"
|
||||
version = "0.5.3"
|
||||
@@ -6428,11 +6410,23 @@ checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"errno",
|
||||
"itoa",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.12",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix-openpty"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a25c3aad9fc1424eb82c88087789a7d938e1829724f3e4043163baf0d13cfc12"
|
||||
dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"rustix 0.38.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.19.1"
|
||||
@@ -6901,15 +6895,6 @@ dependencies = [
|
||||
"syn 2.0.37",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
@@ -6922,18 +6907,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml"
|
||||
version = "0.8.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b"
|
||||
dependencies = [
|
||||
"indexmap 1.9.3",
|
||||
"ryu",
|
||||
"serde",
|
||||
"yaml-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "settings"
|
||||
version = "0.1.0"
|
||||
@@ -6955,7 +6928,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_json_lenient",
|
||||
"smallvec",
|
||||
"toml 0.5.11",
|
||||
"toml",
|
||||
"tree-sitter",
|
||||
"tree-sitter-json 0.19.0",
|
||||
"unindent",
|
||||
@@ -7061,18 +7034,6 @@ dependencies = [
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-mio"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio 0.6.23",
|
||||
"mio-uds",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.1"
|
||||
@@ -7263,12 +7224,6 @@ dependencies = [
|
||||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spsc-buffer"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be6c3f39c37a4283ee4b43d1311c828f2e1fb0541e76ea0cb1a2abd9ef2f5b3b"
|
||||
|
||||
[[package]]
|
||||
name = "sqlez"
|
||||
version = "0.1.0"
|
||||
@@ -7942,7 +7897,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"story",
|
||||
"toml 0.5.11",
|
||||
"toml",
|
||||
"util",
|
||||
"uuid 1.4.1",
|
||||
]
|
||||
@@ -8247,26 +8202,11 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
@@ -8275,8 +8215,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
|
||||
dependencies = [
|
||||
"indexmap 2.0.0",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
]
|
||||
@@ -9098,10 +9036,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "vte"
|
||||
version = "0.11.1"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197"
|
||||
checksum = "40eb22ae96f050e0c0d6f7ce43feeae26c348fc4dea56928ca81537cfaa6188b"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"cursor-icon",
|
||||
"log",
|
||||
"serde",
|
||||
"utf8parse",
|
||||
@@ -9675,15 +9615,6 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
|
||||
|
||||
[[package]]
|
||||
name = "yaml-rust"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
|
||||
dependencies = [
|
||||
"linked-hash-map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "0.5.1"
|
||||
@@ -9704,7 +9635,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.120.0"
|
||||
version = "0.120.6"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"ai",
|
||||
@@ -9793,7 +9724,7 @@ dependencies = [
|
||||
"theme_selector",
|
||||
"thiserror",
|
||||
"tiny_http",
|
||||
"toml 0.5.11",
|
||||
"toml",
|
||||
"tree-sitter",
|
||||
"tree-sitter-bash",
|
||||
"tree-sitter-c",
|
||||
|
||||
@@ -199,9 +199,13 @@ impl AssistantPanel {
|
||||
.update(cx, |toolbar, cx| toolbar.focus_changed(true, cx));
|
||||
cx.notify();
|
||||
if self.focus_handle.is_focused(cx) {
|
||||
if let Some(editor) = self.active_editor() {
|
||||
cx.focus_view(editor);
|
||||
} else if let Some(api_key_editor) = self.api_key_editor.as_ref() {
|
||||
if self.has_credentials() {
|
||||
if let Some(editor) = self.active_editor() {
|
||||
cx.focus_view(editor);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(api_key_editor) = self.api_key_editor.as_ref() {
|
||||
cx.focus_view(api_key_editor);
|
||||
}
|
||||
}
|
||||
@@ -777,6 +781,10 @@ impl AssistantPanel {
|
||||
});
|
||||
}
|
||||
|
||||
fn build_api_key_editor(&mut self, cx: &mut WindowContext<'_>) {
|
||||
self.api_key_editor = Some(build_api_key_editor(cx));
|
||||
}
|
||||
|
||||
fn new_conversation(&mut self, cx: &mut ViewContext<Self>) -> View<ConversationEditor> {
|
||||
let editor = cx.new_view(|cx| {
|
||||
ConversationEditor::new(
|
||||
@@ -870,7 +878,7 @@ impl AssistantPanel {
|
||||
cx.update(|cx| completion_provider.delete_credentials(cx))?
|
||||
.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.api_key_editor = Some(build_api_key_editor(cx));
|
||||
this.build_api_key_editor(cx);
|
||||
this.focus_handle.focus(cx);
|
||||
cx.notify();
|
||||
})
|
||||
@@ -1135,7 +1143,7 @@ impl AssistantPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_api_key_editor(cx: &mut ViewContext<AssistantPanel>) -> View<Editor> {
|
||||
fn build_api_key_editor(cx: &mut WindowContext) -> View<Editor> {
|
||||
cx.new_view(|cx| {
|
||||
let mut editor = Editor::single_line(cx);
|
||||
editor.set_placeholder_text("sk-000000000000000000000000000000000000000000000000", cx);
|
||||
@@ -1146,9 +1154,10 @@ fn build_api_key_editor(cx: &mut ViewContext<AssistantPanel>) -> View<Editor> {
|
||||
impl Render for AssistantPanel {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
if let Some(api_key_editor) = self.api_key_editor.clone() {
|
||||
const INSTRUCTIONS: [&'static str; 5] = [
|
||||
const INSTRUCTIONS: [&'static str; 6] = [
|
||||
"To use the assistant panel or inline assistant, you need to add your OpenAI API key.",
|
||||
" - You can create an API key at: platform.openai.com/api-keys",
|
||||
" - Make sure your OpenAI account has credits",
|
||||
" - Having a subscription for another service like GitHub Copilot won't work.",
|
||||
" ",
|
||||
"Paste your OpenAI API key and press Enter to use the assistant:"
|
||||
@@ -1341,7 +1350,9 @@ impl Panel for AssistantPanel {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
load_credentials.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if this.editors.is_empty() {
|
||||
if !this.has_credentials() {
|
||||
this.build_api_key_editor(cx);
|
||||
} else if this.editors.is_empty() {
|
||||
this.new_conversation(cx);
|
||||
}
|
||||
})
|
||||
@@ -2959,6 +2970,7 @@ impl InlineAssistant {
|
||||
cx.prompt(
|
||||
PromptLevel::Info,
|
||||
prompt_text.as_str(),
|
||||
None,
|
||||
&["Continue", "Cancel"],
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -130,7 +130,8 @@ pub fn check(_: &Check, cx: &mut WindowContext) {
|
||||
} else {
|
||||
drop(cx.prompt(
|
||||
gpui::PromptLevel::Info,
|
||||
"Auto-updates disabled for non-bundled app.",
|
||||
"Could not check for updates",
|
||||
Some("Auto-updates disabled for non-bundled app."),
|
||||
&["Ok"],
|
||||
));
|
||||
}
|
||||
|
||||
@@ -61,11 +61,12 @@ impl ChannelBuffer {
|
||||
.map(language::proto::deserialize_operation)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let buffer = cx.new_model(|_| {
|
||||
let buffer = cx.new_model(|cx| {
|
||||
let capability = channel_store.read(cx).channel_capability(channel.id);
|
||||
language::Buffer::remote(
|
||||
response.buffer_id,
|
||||
response.replica_id as u16,
|
||||
channel.channel_buffer_capability(),
|
||||
capability,
|
||||
base_text,
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -13,11 +13,11 @@ use gpui::{
|
||||
};
|
||||
use language::Capability;
|
||||
use rpc::{
|
||||
proto::{self, ChannelVisibility},
|
||||
proto::{self, ChannelRole, ChannelVisibility},
|
||||
TypedEnvelope,
|
||||
};
|
||||
use std::{mem, sync::Arc, time::Duration};
|
||||
use util::{async_maybe, ResultExt};
|
||||
use util::{async_maybe, maybe, ResultExt};
|
||||
|
||||
pub fn init(client: &Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
|
||||
let channel_store =
|
||||
@@ -29,33 +29,47 @@ pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
pub type ChannelId = u64;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct NotesVersion {
|
||||
epoch: u64,
|
||||
version: clock::Global,
|
||||
}
|
||||
|
||||
pub struct ChannelStore {
|
||||
pub channel_index: ChannelIndex,
|
||||
channel_invitations: Vec<Arc<Channel>>,
|
||||
channel_participants: HashMap<ChannelId, Vec<Arc<User>>>,
|
||||
channel_states: HashMap<ChannelId, ChannelState>,
|
||||
|
||||
outgoing_invites: HashSet<(ChannelId, UserId)>,
|
||||
update_channels_tx: mpsc::UnboundedSender<proto::UpdateChannels>,
|
||||
opened_buffers: HashMap<ChannelId, OpenedModelHandle<ChannelBuffer>>,
|
||||
opened_chats: HashMap<ChannelId, OpenedModelHandle<ChannelChat>>,
|
||||
client: Arc<Client>,
|
||||
user_store: Model<UserStore>,
|
||||
_rpc_subscription: Subscription,
|
||||
_rpc_subscriptions: [Subscription; 2],
|
||||
_watch_connection_status: Task<Option<()>>,
|
||||
disconnect_channel_buffers_task: Option<Task<()>>,
|
||||
_update_channels: Task<()>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Channel {
|
||||
pub id: ChannelId,
|
||||
pub name: SharedString,
|
||||
pub visibility: proto::ChannelVisibility,
|
||||
pub role: proto::ChannelRole,
|
||||
pub unseen_note_version: Option<(u64, clock::Global)>,
|
||||
pub unseen_message_id: Option<u64>,
|
||||
pub parent_path: Vec<u64>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ChannelState {
|
||||
latest_chat_message: Option<u64>,
|
||||
latest_notes_versions: Option<NotesVersion>,
|
||||
observed_chat_message: Option<u64>,
|
||||
observed_notes_versions: Option<NotesVersion>,
|
||||
role: Option<ChannelRole>,
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
pub fn link(&self) -> String {
|
||||
RELEASE_CHANNEL.link_prefix().to_owned()
|
||||
@@ -65,6 +79,17 @@ impl Channel {
|
||||
+ &self.id.to_string()
|
||||
}
|
||||
|
||||
pub fn is_root_channel(&self) -> bool {
|
||||
self.parent_path.is_empty()
|
||||
}
|
||||
|
||||
pub fn root_id(&self) -> ChannelId {
|
||||
self.parent_path
|
||||
.first()
|
||||
.map(|id| *id as ChannelId)
|
||||
.unwrap_or(self.id)
|
||||
}
|
||||
|
||||
pub fn slug(&self) -> String {
|
||||
let slug: String = self
|
||||
.name
|
||||
@@ -74,14 +99,6 @@ impl Channel {
|
||||
|
||||
slug.trim_matches(|c| c == '-').to_string()
|
||||
}
|
||||
|
||||
pub fn channel_buffer_capability(&self) -> Capability {
|
||||
if self.role == proto::ChannelRole::Member || self.role == proto::ChannelRole::Admin {
|
||||
Capability::ReadWrite
|
||||
} else {
|
||||
Capability::ReadOnly
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ChannelMembership {
|
||||
@@ -100,8 +117,7 @@ impl ChannelMembership {
|
||||
},
|
||||
kind_order: match self.kind {
|
||||
proto::channel_member::Kind::Member => 0,
|
||||
proto::channel_member::Kind::AncestorMember => 1,
|
||||
proto::channel_member::Kind::Invitee => 2,
|
||||
proto::channel_member::Kind::Invitee => 1,
|
||||
},
|
||||
username_order: self.user.github_login.as_str(),
|
||||
}
|
||||
@@ -137,8 +153,10 @@ impl ChannelStore {
|
||||
user_store: Model<UserStore>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let rpc_subscription =
|
||||
client.add_message_handler(cx.weak_model(), Self::handle_update_channels);
|
||||
let rpc_subscriptions = [
|
||||
client.add_message_handler(cx.weak_model(), Self::handle_update_channels),
|
||||
client.add_message_handler(cx.weak_model(), Self::handle_update_user_channels),
|
||||
];
|
||||
|
||||
let mut connection_status = client.status();
|
||||
let (update_channels_tx, mut update_channels_rx) = mpsc::unbounded();
|
||||
@@ -175,7 +193,7 @@ impl ChannelStore {
|
||||
update_channels_tx,
|
||||
client,
|
||||
user_store,
|
||||
_rpc_subscription: rpc_subscription,
|
||||
_rpc_subscriptions: rpc_subscriptions,
|
||||
_watch_connection_status: watch_connection_status,
|
||||
disconnect_channel_buffers_task: None,
|
||||
_update_channels: cx.spawn(|this, mut cx| async move {
|
||||
@@ -195,6 +213,7 @@ impl ChannelStore {
|
||||
.await
|
||||
.log_err();
|
||||
}),
|
||||
channel_states: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,39 +325,16 @@ impl ChannelStore {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn has_channel_buffer_changed(&self, channel_id: ChannelId) -> Option<bool> {
|
||||
self.channel_index
|
||||
.by_id()
|
||||
pub fn has_channel_buffer_changed(&self, channel_id: ChannelId) -> bool {
|
||||
self.channel_states
|
||||
.get(&channel_id)
|
||||
.map(|channel| channel.unseen_note_version.is_some())
|
||||
.is_some_and(|state| state.has_channel_buffer_changed())
|
||||
}
|
||||
|
||||
pub fn has_new_messages(&self, channel_id: ChannelId) -> Option<bool> {
|
||||
self.channel_index
|
||||
.by_id()
|
||||
pub fn has_new_messages(&self, channel_id: ChannelId) -> bool {
|
||||
self.channel_states
|
||||
.get(&channel_id)
|
||||
.map(|channel| channel.unseen_message_id.is_some())
|
||||
}
|
||||
|
||||
pub fn notes_changed(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
epoch: u64,
|
||||
version: &clock::Global,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.channel_index.note_changed(channel_id, epoch, version);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn new_message(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
message_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.channel_index.new_message(channel_id, message_id);
|
||||
cx.notify();
|
||||
.is_some_and(|state| state.has_new_messages())
|
||||
}
|
||||
|
||||
pub fn acknowledge_message_id(
|
||||
@@ -347,8 +343,23 @@ impl ChannelStore {
|
||||
message_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.channel_index
|
||||
.acknowledge_message_id(channel_id, message_id);
|
||||
self.channel_states
|
||||
.entry(channel_id)
|
||||
.or_insert_with(|| Default::default())
|
||||
.acknowledge_message_id(message_id);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn update_latest_message_id(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
message_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.channel_states
|
||||
.entry(channel_id)
|
||||
.or_insert_with(|| Default::default())
|
||||
.update_latest_message_id(message_id);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -359,9 +370,25 @@ impl ChannelStore {
|
||||
version: &clock::Global,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.channel_index
|
||||
.acknowledge_note_version(channel_id, epoch, version);
|
||||
cx.notify();
|
||||
self.channel_states
|
||||
.entry(channel_id)
|
||||
.or_insert_with(|| Default::default())
|
||||
.acknowledge_notes_version(epoch, version);
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
pub fn update_latest_notes_version(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
epoch: u64,
|
||||
version: &clock::Global,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.channel_states
|
||||
.entry(channel_id)
|
||||
.or_insert_with(|| Default::default())
|
||||
.update_latest_notes_version(epoch, version);
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
pub fn open_channel_chat(
|
||||
@@ -454,10 +481,42 @@ impl ChannelStore {
|
||||
}
|
||||
|
||||
pub fn is_channel_admin(&self, channel_id: ChannelId) -> bool {
|
||||
let Some(channel) = self.channel_for_id(channel_id) else {
|
||||
return false;
|
||||
};
|
||||
channel.role == proto::ChannelRole::Admin
|
||||
self.channel_role(channel_id) == proto::ChannelRole::Admin
|
||||
}
|
||||
|
||||
pub fn is_root_channel(&self, channel_id: ChannelId) -> bool {
|
||||
self.channel_index
|
||||
.by_id()
|
||||
.get(&channel_id)
|
||||
.map_or(false, |channel| channel.is_root_channel())
|
||||
}
|
||||
|
||||
pub fn is_public_channel(&self, channel_id: ChannelId) -> bool {
|
||||
self.channel_index
|
||||
.by_id()
|
||||
.get(&channel_id)
|
||||
.map_or(false, |channel| {
|
||||
channel.visibility == ChannelVisibility::Public
|
||||
})
|
||||
}
|
||||
|
||||
pub fn channel_capability(&self, channel_id: ChannelId) -> Capability {
|
||||
match self.channel_role(channel_id) {
|
||||
ChannelRole::Admin | ChannelRole::Member => Capability::ReadWrite,
|
||||
_ => Capability::ReadOnly,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn channel_role(&self, channel_id: ChannelId) -> proto::ChannelRole {
|
||||
maybe!({
|
||||
let mut channel = self.channel_for_id(channel_id)?;
|
||||
if !channel.is_root_channel() {
|
||||
channel = self.channel_for_id(channel.root_id())?;
|
||||
}
|
||||
let root_channel_state = self.channel_states.get(&channel.id);
|
||||
root_channel_state?.role
|
||||
})
|
||||
.unwrap_or(proto::ChannelRole::Guest)
|
||||
}
|
||||
|
||||
pub fn channel_participants(&self, channel_id: ChannelId) -> &[Arc<User>] {
|
||||
@@ -508,7 +567,7 @@ impl ChannelStore {
|
||||
pub fn move_channel(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
to: Option<ChannelId>,
|
||||
to: ChannelId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
@@ -747,6 +806,36 @@ impl ChannelStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_update_user_channels(
|
||||
this: Model<Self>,
|
||||
message: TypedEnvelope<proto::UpdateUserChannels>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
for buffer_version in message.payload.observed_channel_buffer_version {
|
||||
let version = language::proto::deserialize_version(&buffer_version.version);
|
||||
this.acknowledge_notes_version(
|
||||
buffer_version.channel_id,
|
||||
buffer_version.epoch,
|
||||
&version,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
for message_id in message.payload.observed_channel_message_id {
|
||||
this.acknowledge_message_id(message_id.channel_id, message_id.message_id, cx);
|
||||
}
|
||||
for membership in message.payload.channel_memberships {
|
||||
if let Some(role) = ChannelRole::from_i32(membership.role) {
|
||||
this.channel_states
|
||||
.entry(membership.channel_id)
|
||||
.or_insert_with(|| ChannelState::default())
|
||||
.set_role(role)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_connect(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
self.channel_index.clear();
|
||||
self.channel_invitations.clear();
|
||||
@@ -909,10 +998,7 @@ impl ChannelStore {
|
||||
Arc::new(Channel {
|
||||
id: channel.id,
|
||||
visibility: channel.visibility(),
|
||||
role: channel.role(),
|
||||
name: channel.name.into(),
|
||||
unseen_note_version: None,
|
||||
unseen_message_id: None,
|
||||
parent_path: channel.parent_path,
|
||||
}),
|
||||
),
|
||||
@@ -921,8 +1007,8 @@ impl ChannelStore {
|
||||
|
||||
let channels_changed = !payload.channels.is_empty()
|
||||
|| !payload.delete_channels.is_empty()
|
||||
|| !payload.unseen_channel_messages.is_empty()
|
||||
|| !payload.unseen_channel_buffer_changes.is_empty();
|
||||
|| !payload.latest_channel_message_ids.is_empty()
|
||||
|| !payload.latest_channel_buffer_versions.is_empty();
|
||||
|
||||
if channels_changed {
|
||||
if !payload.delete_channels.is_empty() {
|
||||
@@ -963,20 +1049,19 @@ impl ChannelStore {
|
||||
}
|
||||
}
|
||||
|
||||
for unseen_buffer_change in payload.unseen_channel_buffer_changes {
|
||||
let version = language::proto::deserialize_version(&unseen_buffer_change.version);
|
||||
index.note_changed(
|
||||
unseen_buffer_change.channel_id,
|
||||
unseen_buffer_change.epoch,
|
||||
&version,
|
||||
);
|
||||
for latest_buffer_version in payload.latest_channel_buffer_versions {
|
||||
let version = language::proto::deserialize_version(&latest_buffer_version.version);
|
||||
self.channel_states
|
||||
.entry(latest_buffer_version.channel_id)
|
||||
.or_default()
|
||||
.update_latest_notes_version(latest_buffer_version.epoch, &version)
|
||||
}
|
||||
|
||||
for unseen_channel_message in payload.unseen_channel_messages {
|
||||
index.new_messages(
|
||||
unseen_channel_message.channel_id,
|
||||
unseen_channel_message.message_id,
|
||||
);
|
||||
for latest_channel_message in payload.latest_channel_message_ids {
|
||||
self.channel_states
|
||||
.entry(latest_channel_message.channel_id)
|
||||
.or_default()
|
||||
.update_latest_message_id(latest_channel_message.message_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1025,3 +1110,69 @@ impl ChannelStore {
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl ChannelState {
|
||||
fn set_role(&mut self, role: ChannelRole) {
|
||||
self.role = Some(role);
|
||||
}
|
||||
|
||||
fn has_channel_buffer_changed(&self) -> bool {
|
||||
if let Some(latest_version) = &self.latest_notes_versions {
|
||||
if let Some(observed_version) = &self.observed_notes_versions {
|
||||
latest_version.epoch > observed_version.epoch
|
||||
|| latest_version
|
||||
.version
|
||||
.changed_since(&observed_version.version)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn has_new_messages(&self) -> bool {
|
||||
let latest_message_id = self.latest_chat_message;
|
||||
let observed_message_id = self.observed_chat_message;
|
||||
|
||||
latest_message_id.is_some_and(|latest_message_id| {
|
||||
latest_message_id > observed_message_id.unwrap_or_default()
|
||||
})
|
||||
}
|
||||
|
||||
fn acknowledge_message_id(&mut self, message_id: u64) {
|
||||
let observed = self.observed_chat_message.get_or_insert(message_id);
|
||||
*observed = (*observed).max(message_id);
|
||||
}
|
||||
|
||||
fn update_latest_message_id(&mut self, message_id: u64) {
|
||||
self.latest_chat_message =
|
||||
Some(message_id.max(self.latest_chat_message.unwrap_or_default()));
|
||||
}
|
||||
|
||||
fn acknowledge_notes_version(&mut self, epoch: u64, version: &clock::Global) {
|
||||
if let Some(existing) = &mut self.observed_notes_versions {
|
||||
if existing.epoch == epoch {
|
||||
existing.version.join(version);
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.observed_notes_versions = Some(NotesVersion {
|
||||
epoch,
|
||||
version: version.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
fn update_latest_notes_version(&mut self, epoch: u64, version: &clock::Global) {
|
||||
if let Some(existing) = &mut self.latest_notes_versions {
|
||||
if existing.epoch == epoch {
|
||||
existing.version.join(version);
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.latest_notes_versions = Some(NotesVersion {
|
||||
epoch,
|
||||
version: version.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,43 +37,6 @@ impl ChannelIndex {
|
||||
channels_by_id: &mut self.channels_by_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn acknowledge_note_version(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
epoch: u64,
|
||||
version: &clock::Global,
|
||||
) {
|
||||
if let Some(channel) = self.channels_by_id.get_mut(&channel_id) {
|
||||
let channel = Arc::make_mut(channel);
|
||||
if let Some((unseen_epoch, unseen_version)) = &channel.unseen_note_version {
|
||||
if epoch > *unseen_epoch
|
||||
|| epoch == *unseen_epoch && version.observed_all(unseen_version)
|
||||
{
|
||||
channel.unseen_note_version = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn acknowledge_message_id(&mut self, channel_id: ChannelId, message_id: u64) {
|
||||
if let Some(channel) = self.channels_by_id.get_mut(&channel_id) {
|
||||
let channel = Arc::make_mut(channel);
|
||||
if let Some(unseen_message_id) = channel.unseen_message_id {
|
||||
if message_id >= unseen_message_id {
|
||||
channel.unseen_message_id = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn note_changed(&mut self, channel_id: ChannelId, epoch: u64, version: &clock::Global) {
|
||||
insert_note_changed(&mut self.channels_by_id, channel_id, epoch, version);
|
||||
}
|
||||
|
||||
pub fn new_message(&mut self, channel_id: ChannelId, message_id: u64) {
|
||||
insert_new_message(&mut self.channels_by_id, channel_id, message_id)
|
||||
}
|
||||
}
|
||||
|
||||
/// A guard for ensuring that the paths index maintains its sort and uniqueness
|
||||
@@ -85,36 +48,25 @@ pub struct ChannelPathsInsertGuard<'a> {
|
||||
}
|
||||
|
||||
impl<'a> ChannelPathsInsertGuard<'a> {
|
||||
pub fn note_changed(&mut self, channel_id: ChannelId, epoch: u64, version: &clock::Global) {
|
||||
insert_note_changed(self.channels_by_id, channel_id, epoch, version);
|
||||
}
|
||||
|
||||
pub fn new_messages(&mut self, channel_id: ChannelId, message_id: u64) {
|
||||
insert_new_message(self.channels_by_id, channel_id, message_id)
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, channel_proto: proto::Channel) -> bool {
|
||||
let mut ret = false;
|
||||
if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) {
|
||||
let existing_channel = Arc::make_mut(existing_channel);
|
||||
|
||||
ret = existing_channel.visibility != channel_proto.visibility()
|
||||
|| existing_channel.role != channel_proto.role()
|
||||
|| existing_channel.name != channel_proto.name;
|
||||
|| existing_channel.name != channel_proto.name
|
||||
|| existing_channel.parent_path != channel_proto.parent_path;
|
||||
|
||||
existing_channel.visibility = channel_proto.visibility();
|
||||
existing_channel.role = channel_proto.role();
|
||||
existing_channel.name = channel_proto.name.into();
|
||||
existing_channel.parent_path = channel_proto.parent_path.into();
|
||||
} else {
|
||||
self.channels_by_id.insert(
|
||||
channel_proto.id,
|
||||
Arc::new(Channel {
|
||||
id: channel_proto.id,
|
||||
visibility: channel_proto.visibility(),
|
||||
role: channel_proto.role(),
|
||||
name: channel_proto.name.into(),
|
||||
unseen_note_version: None,
|
||||
unseen_message_id: None,
|
||||
parent_path: channel_proto.parent_path,
|
||||
}),
|
||||
);
|
||||
@@ -153,32 +105,3 @@ fn channel_path_sorting_key<'a>(
|
||||
.filter_map(|id| Some(channels_by_id.get(id)?.name.as_ref()))
|
||||
.chain(name)
|
||||
}
|
||||
|
||||
fn insert_note_changed(
|
||||
channels_by_id: &mut BTreeMap<ChannelId, Arc<Channel>>,
|
||||
channel_id: u64,
|
||||
epoch: u64,
|
||||
version: &clock::Global,
|
||||
) {
|
||||
if let Some(channel) = channels_by_id.get_mut(&channel_id) {
|
||||
let unseen_version = Arc::make_mut(channel)
|
||||
.unseen_note_version
|
||||
.get_or_insert((0, clock::Global::new()));
|
||||
if epoch > unseen_version.0 {
|
||||
*unseen_version = (epoch, version.clone());
|
||||
} else {
|
||||
unseen_version.1.join(version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_new_message(
|
||||
channels_by_id: &mut BTreeMap<ChannelId, Arc<Channel>>,
|
||||
channel_id: u64,
|
||||
message_id: u64,
|
||||
) {
|
||||
if let Some(channel) = channels_by_id.get_mut(&channel_id) {
|
||||
let unseen_message_id = Arc::make_mut(channel).unseen_message_id.get_or_insert(0);
|
||||
*unseen_message_id = message_id.max(*unseen_message_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,14 +19,12 @@ fn test_update_channels(cx: &mut AppContext) {
|
||||
id: 1,
|
||||
name: "b".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
parent_path: Vec::new(),
|
||||
},
|
||||
proto::Channel {
|
||||
id: 2,
|
||||
name: "a".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
parent_path: Vec::new(),
|
||||
},
|
||||
],
|
||||
@@ -38,8 +36,8 @@ fn test_update_channels(cx: &mut AppContext) {
|
||||
&channel_store,
|
||||
&[
|
||||
//
|
||||
(0, "a".to_string(), proto::ChannelRole::Member),
|
||||
(0, "b".to_string(), proto::ChannelRole::Admin),
|
||||
(0, "a".to_string()),
|
||||
(0, "b".to_string()),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
@@ -52,14 +50,12 @@ fn test_update_channels(cx: &mut AppContext) {
|
||||
id: 3,
|
||||
name: "x".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
parent_path: vec![1],
|
||||
},
|
||||
proto::Channel {
|
||||
id: 4,
|
||||
name: "y".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
parent_path: vec![2],
|
||||
},
|
||||
],
|
||||
@@ -70,10 +66,10 @@ fn test_update_channels(cx: &mut AppContext) {
|
||||
assert_channels(
|
||||
&channel_store,
|
||||
&[
|
||||
(0, "a".to_string(), proto::ChannelRole::Member),
|
||||
(1, "y".to_string(), proto::ChannelRole::Member),
|
||||
(0, "b".to_string(), proto::ChannelRole::Admin),
|
||||
(1, "x".to_string(), proto::ChannelRole::Admin),
|
||||
(0, "a".to_string()),
|
||||
(1, "y".to_string()),
|
||||
(0, "b".to_string()),
|
||||
(1, "x".to_string()),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
@@ -91,21 +87,18 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
|
||||
id: 0,
|
||||
name: "a".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
parent_path: vec![],
|
||||
},
|
||||
proto::Channel {
|
||||
id: 1,
|
||||
name: "b".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
parent_path: vec![0],
|
||||
},
|
||||
proto::Channel {
|
||||
id: 2,
|
||||
name: "c".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
parent_path: vec![0, 1],
|
||||
},
|
||||
],
|
||||
@@ -118,9 +111,9 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
|
||||
&channel_store,
|
||||
&[
|
||||
//
|
||||
(0, "a".to_string(), proto::ChannelRole::Admin),
|
||||
(1, "b".to_string(), proto::ChannelRole::Admin),
|
||||
(2, "c".to_string(), proto::ChannelRole::Admin),
|
||||
(0, "a".to_string()),
|
||||
(1, "b".to_string()),
|
||||
(2, "c".to_string()),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
@@ -135,11 +128,7 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
|
||||
);
|
||||
|
||||
// Make sure that the 1/2/3 path is gone
|
||||
assert_channels(
|
||||
&channel_store,
|
||||
&[(0, "a".to_string(), proto::ChannelRole::Admin)],
|
||||
cx,
|
||||
);
|
||||
assert_channels(&channel_store, &[(0, "a".to_string())], cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -156,18 +145,13 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||
id: channel_id,
|
||||
name: "the-channel".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
parent_path: vec![],
|
||||
}],
|
||||
..Default::default()
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
cx.update(|cx| {
|
||||
assert_channels(
|
||||
&channel_store,
|
||||
&[(0, "the-channel".to_string(), proto::ChannelRole::Member)],
|
||||
cx,
|
||||
);
|
||||
assert_channels(&channel_store, &[(0, "the-channel".to_string())], cx);
|
||||
});
|
||||
|
||||
let get_users = server.receive::<proto::GetUsers>().await.unwrap();
|
||||
@@ -368,13 +352,13 @@ fn update_channels(
|
||||
#[track_caller]
|
||||
fn assert_channels(
|
||||
channel_store: &Model<ChannelStore>,
|
||||
expected_channels: &[(usize, String, proto::ChannelRole)],
|
||||
expected_channels: &[(usize, String)],
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let actual = channel_store.update(cx, |store, _| {
|
||||
store
|
||||
.ordered_channels()
|
||||
.map(|(depth, channel)| (depth, channel.name.to_string(), channel.role))
|
||||
.map(|(depth, channel)| (depth, channel.name.to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
assert_eq!(actual, expected_channels);
|
||||
|
||||
@@ -689,12 +689,7 @@ impl Client {
|
||||
Ok(())
|
||||
}
|
||||
Err(error) => {
|
||||
client.respond_with_error(
|
||||
receipt,
|
||||
proto::Error {
|
||||
message: format!("{:?}", error),
|
||||
},
|
||||
)?;
|
||||
client.respond_with_error(receipt, error.to_proto())?;
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.40.1"
|
||||
version = "0.40.2"
|
||||
publish = false
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ use std::{
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use tables::*;
|
||||
pub use tables::*;
|
||||
use tokio::sync::{Mutex, OwnedMutexGuard};
|
||||
|
||||
pub use ids::*;
|
||||
@@ -169,6 +169,30 @@ impl Database {
|
||||
self.run(body).await
|
||||
}
|
||||
|
||||
pub async fn weak_transaction<F, Fut, T>(&self, f: F) -> Result<T>
|
||||
where
|
||||
F: Send + Fn(TransactionHandle) -> Fut,
|
||||
Fut: Send + Future<Output = Result<T>>,
|
||||
{
|
||||
let body = async {
|
||||
let (tx, result) = self.with_weak_transaction(&f).await?;
|
||||
match result {
|
||||
Ok(result) => match tx.commit().await.map_err(Into::into) {
|
||||
Ok(()) => return Ok(result),
|
||||
Err(error) => {
|
||||
return Err(error);
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
tx.rollback().await?;
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.run(body).await
|
||||
}
|
||||
|
||||
/// The same as room_transaction, but if you need to only optionally return a Room.
|
||||
async fn optional_room_transaction<F, Fut, T>(&self, f: F) -> Result<Option<RoomGuard<T>>>
|
||||
where
|
||||
@@ -284,6 +308,30 @@ impl Database {
|
||||
Ok((tx, result))
|
||||
}
|
||||
|
||||
async fn with_weak_transaction<F, Fut, T>(
|
||||
&self,
|
||||
f: &F,
|
||||
) -> Result<(DatabaseTransaction, Result<T>)>
|
||||
where
|
||||
F: Send + Fn(TransactionHandle) -> Fut,
|
||||
Fut: Send + Future<Output = Result<T>>,
|
||||
{
|
||||
let tx = self
|
||||
.pool
|
||||
.begin_with_config(Some(IsolationLevel::ReadCommitted), None)
|
||||
.await?;
|
||||
|
||||
let mut tx = Arc::new(Some(tx));
|
||||
let result = f(TransactionHandle(tx.clone())).await;
|
||||
let Some(tx) = Arc::get_mut(&mut tx).and_then(|tx| tx.take()) else {
|
||||
return Err(anyhow!(
|
||||
"couldn't complete transaction because it's still in use"
|
||||
))?;
|
||||
};
|
||||
|
||||
Ok((tx, result))
|
||||
}
|
||||
|
||||
async fn run<F, T>(&self, future: F) -> Result<T>
|
||||
where
|
||||
F: Future<Output = Result<T>>,
|
||||
@@ -303,13 +351,14 @@ impl Database {
|
||||
}
|
||||
}
|
||||
|
||||
async fn retry_on_serialization_error(&self, error: &Error, prev_attempt_count: u32) -> bool {
|
||||
async fn retry_on_serialization_error(&self, error: &Error, prev_attempt_count: usize) -> bool {
|
||||
// If the error is due to a failure to serialize concurrent transactions, then retry
|
||||
// this transaction after a delay. With each subsequent retry, double the delay duration.
|
||||
// Also vary the delay randomly in order to ensure different database connections retry
|
||||
// at different times.
|
||||
if is_serialization_error(error) {
|
||||
let base_delay = 4_u64 << prev_attempt_count.min(16);
|
||||
const SLEEPS: [f32; 10] = [10., 20., 40., 80., 160., 320., 640., 1280., 2560., 5120.];
|
||||
if is_serialization_error(error) && prev_attempt_count < SLEEPS.len() {
|
||||
let base_delay = SLEEPS[prev_attempt_count];
|
||||
let randomized_delay = base_delay as f32 * self.rng.lock().await.gen_range(0.5..=2.0);
|
||||
log::info!(
|
||||
"retrying transaction after serialization error. delay: {} ms.",
|
||||
@@ -453,36 +502,6 @@ pub struct NewUserResult {
|
||||
pub signup_device_id: Option<String>,
|
||||
}
|
||||
|
||||
/// The result of moving a channel.
|
||||
#[derive(Debug)]
|
||||
pub struct MoveChannelResult {
|
||||
pub participants_to_update: HashMap<UserId, ChannelsForUser>,
|
||||
pub participants_to_remove: HashSet<UserId>,
|
||||
pub moved_channels: HashSet<ChannelId>,
|
||||
}
|
||||
|
||||
/// The result of renaming a channel.
|
||||
#[derive(Debug)]
|
||||
pub struct RenameChannelResult {
|
||||
pub channel: Channel,
|
||||
pub participants_to_update: HashMap<UserId, Channel>,
|
||||
}
|
||||
|
||||
/// The result of creating a channel.
|
||||
#[derive(Debug)]
|
||||
pub struct CreateChannelResult {
|
||||
pub channel: Channel,
|
||||
pub participants_to_update: Vec<(UserId, ChannelsForUser)>,
|
||||
}
|
||||
|
||||
/// The result of setting a channel's visibility.
|
||||
#[derive(Debug)]
|
||||
pub struct SetChannelVisibilityResult {
|
||||
pub participants_to_update: HashMap<UserId, ChannelsForUser>,
|
||||
pub participants_to_remove: HashSet<UserId>,
|
||||
pub channels_to_remove: Vec<ChannelId>,
|
||||
}
|
||||
|
||||
/// The result of updating a channel membership.
|
||||
#[derive(Debug)]
|
||||
pub struct MembershipUpdated {
|
||||
@@ -522,18 +541,16 @@ pub struct Channel {
|
||||
pub id: ChannelId,
|
||||
pub name: String,
|
||||
pub visibility: ChannelVisibility,
|
||||
pub role: ChannelRole,
|
||||
/// parent_path is the channel ids from the root to this one (not including this one)
|
||||
pub parent_path: Vec<ChannelId>,
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
fn from_model(value: channel::Model, role: ChannelRole) -> Self {
|
||||
fn from_model(value: channel::Model) -> Self {
|
||||
Channel {
|
||||
id: value.id,
|
||||
visibility: value.visibility,
|
||||
name: value.clone().name,
|
||||
role,
|
||||
parent_path: value.ancestors().collect(),
|
||||
}
|
||||
}
|
||||
@@ -543,7 +560,6 @@ impl Channel {
|
||||
id: self.id.to_proto(),
|
||||
name: self.name.clone(),
|
||||
visibility: self.visibility.into(),
|
||||
role: self.role.into(),
|
||||
parent_path: self.parent_path.iter().map(|c| c.to_proto()).collect(),
|
||||
}
|
||||
}
|
||||
@@ -569,9 +585,10 @@ impl ChannelMember {
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct ChannelsForUser {
|
||||
pub channels: Vec<Channel>,
|
||||
pub channel_memberships: Vec<channel_member::Model>,
|
||||
pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
|
||||
pub unseen_buffer_changes: Vec<proto::UnseenChannelBufferChange>,
|
||||
pub channel_messages: Vec<proto::UnseenChannelMessage>,
|
||||
pub latest_buffer_versions: Vec<proto::ChannelBufferVersion>,
|
||||
pub latest_channel_messages: Vec<proto::ChannelMessageId>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -129,6 +129,15 @@ impl ChannelRole {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_see_channel(&self, visibility: ChannelVisibility) -> bool {
|
||||
use ChannelRole::*;
|
||||
match self {
|
||||
Admin | Member => true,
|
||||
Guest => visibility == ChannelVisibility::Public,
|
||||
Banned => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// True if the role allows access to all descendant channels
|
||||
pub fn can_see_all_descendants(&self) -> bool {
|
||||
use ChannelRole::*;
|
||||
|
||||
@@ -748,18 +748,11 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn unseen_channel_buffer_changes(
|
||||
pub async fn latest_channel_buffer_changes(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
channel_ids: &[ChannelId],
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Vec<proto::UnseenChannelBufferChange>> {
|
||||
#[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
|
||||
enum QueryIds {
|
||||
ChannelId,
|
||||
Id,
|
||||
}
|
||||
|
||||
) -> Result<Vec<proto::ChannelBufferVersion>> {
|
||||
let mut channel_ids_by_buffer_id = HashMap::default();
|
||||
let mut rows = buffer::Entity::find()
|
||||
.filter(buffer::Column::ChannelId.is_in(channel_ids.iter().copied()))
|
||||
@@ -771,51 +764,23 @@ impl Database {
|
||||
}
|
||||
drop(rows);
|
||||
|
||||
let mut observed_edits_by_buffer_id = HashMap::default();
|
||||
let mut rows = observed_buffer_edits::Entity::find()
|
||||
.filter(observed_buffer_edits::Column::UserId.eq(user_id))
|
||||
.filter(
|
||||
observed_buffer_edits::Column::BufferId
|
||||
.is_in(channel_ids_by_buffer_id.keys().copied()),
|
||||
)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
while let Some(row) = rows.next().await {
|
||||
let row = row?;
|
||||
observed_edits_by_buffer_id.insert(row.buffer_id, row);
|
||||
}
|
||||
drop(rows);
|
||||
|
||||
let latest_operations = self
|
||||
.get_latest_operations_for_buffers(channel_ids_by_buffer_id.keys().copied(), &*tx)
|
||||
.await?;
|
||||
|
||||
let mut changes = Vec::default();
|
||||
for latest in latest_operations {
|
||||
if let Some(observed) = observed_edits_by_buffer_id.get(&latest.buffer_id) {
|
||||
if (
|
||||
observed.epoch,
|
||||
observed.lamport_timestamp,
|
||||
observed.replica_id,
|
||||
) >= (latest.epoch, latest.lamport_timestamp, latest.replica_id)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(channel_id) = channel_ids_by_buffer_id.get(&latest.buffer_id) {
|
||||
changes.push(proto::UnseenChannelBufferChange {
|
||||
channel_id: channel_id.to_proto(),
|
||||
epoch: latest.epoch as u64,
|
||||
Ok(latest_operations
|
||||
.iter()
|
||||
.flat_map(|op| {
|
||||
Some(proto::ChannelBufferVersion {
|
||||
channel_id: channel_ids_by_buffer_id.get(&op.buffer_id)?.to_proto(),
|
||||
epoch: op.epoch as u64,
|
||||
version: vec![proto::VectorClockEntry {
|
||||
replica_id: latest.replica_id as u32,
|
||||
timestamp: latest.lamport_timestamp as u32,
|
||||
replica_id: op.replica_id as u32,
|
||||
timestamp: op.lamport_timestamp as u32,
|
||||
}],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(changes)
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Returns the latest operations for the buffers with the specified IDs.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::*;
|
||||
use rpc::proto::channel_member::Kind;
|
||||
use rpc::{proto::channel_member::Kind, ErrorCode, ErrorCodeExt};
|
||||
use sea_orm::TryGetableMany;
|
||||
|
||||
impl Database {
|
||||
@@ -19,11 +19,7 @@ impl Database {
|
||||
|
||||
#[cfg(test)]
|
||||
pub async fn create_root_channel(&self, name: &str, creator_id: UserId) -> Result<ChannelId> {
|
||||
Ok(self
|
||||
.create_channel(name, None, creator_id)
|
||||
.await?
|
||||
.channel
|
||||
.id)
|
||||
Ok(self.create_channel(name, None, creator_id).await?.0.id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -36,7 +32,7 @@ impl Database {
|
||||
Ok(self
|
||||
.create_channel(name, Some(parent), creator_id)
|
||||
.await?
|
||||
.channel
|
||||
.0
|
||||
.id)
|
||||
}
|
||||
|
||||
@@ -46,10 +42,15 @@ impl Database {
|
||||
name: &str,
|
||||
parent_channel_id: Option<ChannelId>,
|
||||
admin_id: UserId,
|
||||
) -> Result<CreateChannelResult> {
|
||||
) -> Result<(
|
||||
Channel,
|
||||
Option<channel_member::Model>,
|
||||
Vec<channel_member::Model>,
|
||||
)> {
|
||||
let name = Self::sanitize_channel_name(name)?;
|
||||
self.transaction(move |tx| async move {
|
||||
let mut parent = None;
|
||||
let mut membership = None;
|
||||
|
||||
if let Some(parent_channel_id) = parent_channel_id {
|
||||
let parent_channel = self.get_channel_internal(parent_channel_id, &*tx).await?;
|
||||
@@ -72,29 +73,26 @@ impl Database {
|
||||
.insert(&*tx)
|
||||
.await?;
|
||||
|
||||
let participants_to_update;
|
||||
if let Some(parent) = &parent {
|
||||
participants_to_update = self
|
||||
.participants_to_notify_for_channel_change(parent, &*tx)
|
||||
.await?;
|
||||
} else {
|
||||
participants_to_update = vec![];
|
||||
if parent.is_none() {
|
||||
membership = Some(
|
||||
channel_member::ActiveModel {
|
||||
id: ActiveValue::NotSet,
|
||||
channel_id: ActiveValue::Set(channel.id),
|
||||
user_id: ActiveValue::Set(admin_id),
|
||||
accepted: ActiveValue::Set(true),
|
||||
role: ActiveValue::Set(ChannelRole::Admin),
|
||||
}
|
||||
.insert(&*tx)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
|
||||
channel_member::ActiveModel {
|
||||
id: ActiveValue::NotSet,
|
||||
channel_id: ActiveValue::Set(channel.id),
|
||||
user_id: ActiveValue::Set(admin_id),
|
||||
accepted: ActiveValue::Set(true),
|
||||
role: ActiveValue::Set(ChannelRole::Admin),
|
||||
}
|
||||
.insert(&*tx)
|
||||
let channel_members = channel_member::Entity::find()
|
||||
.filter(channel_member::Column::ChannelId.eq(channel.root_id()))
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
};
|
||||
|
||||
Ok(CreateChannelResult {
|
||||
channel: Channel::from_model(channel, ChannelRole::Admin),
|
||||
participants_to_update,
|
||||
})
|
||||
Ok((Channel::from_model(channel), membership, channel_members))
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -137,16 +135,9 @@ impl Database {
|
||||
);
|
||||
} else if channel.visibility == ChannelVisibility::Public {
|
||||
role = Some(ChannelRole::Guest);
|
||||
let channel_to_join = self
|
||||
.public_ancestors_including_self(&channel, &*tx)
|
||||
.await?
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap_or(channel.clone());
|
||||
|
||||
channel_member::Entity::insert(channel_member::ActiveModel {
|
||||
id: ActiveValue::NotSet,
|
||||
channel_id: ActiveValue::Set(channel_to_join.id),
|
||||
channel_id: ActiveValue::Set(channel.root_id()),
|
||||
user_id: ActiveValue::Set(user_id),
|
||||
accepted: ActiveValue::Set(true),
|
||||
role: ActiveValue::Set(ChannelRole::Guest),
|
||||
@@ -155,7 +146,7 @@ impl Database {
|
||||
.await?;
|
||||
|
||||
accept_invite_result = Some(
|
||||
self.calculate_membership_updated(&channel_to_join, user_id, &*tx)
|
||||
self.calculate_membership_updated(&channel, user_id, &*tx)
|
||||
.await?,
|
||||
);
|
||||
|
||||
@@ -166,7 +157,7 @@ impl Database {
|
||||
}
|
||||
|
||||
if role.is_none() || role == Some(ChannelRole::Banned) {
|
||||
Err(anyhow!("not allowed"))?
|
||||
Err(ErrorCode::Forbidden.anyhow())?
|
||||
}
|
||||
let role = role.unwrap();
|
||||
|
||||
@@ -188,76 +179,47 @@ impl Database {
|
||||
channel_id: ChannelId,
|
||||
visibility: ChannelVisibility,
|
||||
admin_id: UserId,
|
||||
) -> Result<SetChannelVisibilityResult> {
|
||||
) -> Result<(Channel, Vec<channel_member::Model>)> {
|
||||
self.transaction(move |tx| async move {
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
|
||||
self.check_user_is_channel_admin(&channel, admin_id, &*tx)
|
||||
.await?;
|
||||
|
||||
let previous_members = self
|
||||
.get_channel_participant_details_internal(&channel, &*tx)
|
||||
.await?;
|
||||
if visibility == ChannelVisibility::Public {
|
||||
if let Some(parent_id) = channel.parent_id() {
|
||||
let parent = self.get_channel_internal(parent_id, &*tx).await?;
|
||||
|
||||
if parent.visibility != ChannelVisibility::Public {
|
||||
Err(ErrorCode::BadPublicNesting
|
||||
.with_tag("direction", "parent")
|
||||
.anyhow())?;
|
||||
}
|
||||
}
|
||||
} else if visibility == ChannelVisibility::Members {
|
||||
if self
|
||||
.get_channel_descendants_including_self(vec![channel_id], &*tx)
|
||||
.await?
|
||||
.into_iter()
|
||||
.any(|channel| {
|
||||
channel.id != channel_id && channel.visibility == ChannelVisibility::Public
|
||||
})
|
||||
{
|
||||
Err(ErrorCode::BadPublicNesting
|
||||
.with_tag("direction", "children")
|
||||
.anyhow())?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut model = channel.into_active_model();
|
||||
model.visibility = ActiveValue::Set(visibility);
|
||||
let channel = model.update(&*tx).await?;
|
||||
|
||||
let mut participants_to_update: HashMap<UserId, ChannelsForUser> = self
|
||||
.participants_to_notify_for_channel_change(&channel, &*tx)
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect();
|
||||
let channel_members = channel_member::Entity::find()
|
||||
.filter(channel_member::Column::ChannelId.eq(channel.root_id()))
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut channels_to_remove: Vec<ChannelId> = vec![];
|
||||
let mut participants_to_remove: HashSet<UserId> = HashSet::default();
|
||||
match visibility {
|
||||
ChannelVisibility::Members => {
|
||||
let all_descendents: Vec<ChannelId> = self
|
||||
.get_channel_descendants_including_self(vec![channel_id], &*tx)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|channel| channel.id)
|
||||
.collect();
|
||||
|
||||
channels_to_remove = channel::Entity::find()
|
||||
.filter(
|
||||
channel::Column::Id
|
||||
.is_in(all_descendents)
|
||||
.and(channel::Column::Visibility.eq(ChannelVisibility::Public)),
|
||||
)
|
||||
.all(&*tx)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|channel| channel.id)
|
||||
.collect();
|
||||
|
||||
channels_to_remove.push(channel_id);
|
||||
|
||||
for member in previous_members {
|
||||
if member.role.can_only_see_public_descendants() {
|
||||
participants_to_remove.insert(member.user_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
ChannelVisibility::Public => {
|
||||
if let Some(public_parent) = self.public_parent_channel(&channel, &*tx).await? {
|
||||
let parent_updates = self
|
||||
.participants_to_notify_for_channel_change(&public_parent, &*tx)
|
||||
.await?;
|
||||
|
||||
for (user_id, channels) in parent_updates {
|
||||
participants_to_update.insert(user_id, channels);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(SetChannelVisibilityResult {
|
||||
participants_to_update,
|
||||
participants_to_remove,
|
||||
channels_to_remove,
|
||||
})
|
||||
Ok((Channel::from_model(channel), channel_members))
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -290,7 +252,7 @@ impl Database {
|
||||
.await?;
|
||||
|
||||
let members_to_notify: Vec<UserId> = channel_member::Entity::find()
|
||||
.filter(channel_member::Column::ChannelId.is_in(channel.ancestors_including_self()))
|
||||
.filter(channel_member::Column::ChannelId.eq(channel.root_id()))
|
||||
.select_only()
|
||||
.column(channel_member::Column::UserId)
|
||||
.distinct()
|
||||
@@ -327,6 +289,9 @@ impl Database {
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
self.check_user_is_channel_admin(&channel, inviter_id, &*tx)
|
||||
.await?;
|
||||
if !channel.is_root() {
|
||||
Err(ErrorCode::NotARootChannel.anyhow())?
|
||||
}
|
||||
|
||||
channel_member::ActiveModel {
|
||||
id: ActiveValue::NotSet,
|
||||
@@ -338,7 +303,7 @@ impl Database {
|
||||
.insert(&*tx)
|
||||
.await?;
|
||||
|
||||
let channel = Channel::from_model(channel, role);
|
||||
let channel = Channel::from_model(channel);
|
||||
|
||||
let notifications = self
|
||||
.create_notification(
|
||||
@@ -377,35 +342,24 @@ impl Database {
|
||||
channel_id: ChannelId,
|
||||
admin_id: UserId,
|
||||
new_name: &str,
|
||||
) -> Result<RenameChannelResult> {
|
||||
) -> Result<(Channel, Vec<channel_member::Model>)> {
|
||||
self.transaction(move |tx| async move {
|
||||
let new_name = Self::sanitize_channel_name(new_name)?.to_string();
|
||||
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
let role = self
|
||||
.check_user_is_channel_admin(&channel, admin_id, &*tx)
|
||||
self.check_user_is_channel_admin(&channel, admin_id, &*tx)
|
||||
.await?;
|
||||
|
||||
let mut model = channel.into_active_model();
|
||||
model.name = ActiveValue::Set(new_name.clone());
|
||||
let channel = model.update(&*tx).await?;
|
||||
|
||||
let participants = self
|
||||
.get_channel_participant_details_internal(&channel, &*tx)
|
||||
let channel_members = channel_member::Entity::find()
|
||||
.filter(channel_member::Column::ChannelId.eq(channel.root_id()))
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
Ok(RenameChannelResult {
|
||||
channel: Channel::from_model(channel.clone(), role),
|
||||
participants_to_update: participants
|
||||
.iter()
|
||||
.map(|participant| {
|
||||
(
|
||||
participant.user_id,
|
||||
Channel::from_model(channel.clone(), participant.role),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
Ok((Channel::from_model(channel), channel_members))
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -580,10 +534,7 @@ impl Database {
|
||||
|
||||
let channels = channels
|
||||
.into_iter()
|
||||
.filter_map(|channel| {
|
||||
let role = *role_for_channel.get(&channel.id)?;
|
||||
Some(Channel::from_model(channel, role))
|
||||
})
|
||||
.filter_map(|channel| Some(Channel::from_model(channel)))
|
||||
.collect();
|
||||
|
||||
Ok(channels)
|
||||
@@ -591,6 +542,26 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_channel_memberships(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
) -> Result<(Vec<channel_member::Model>, Vec<channel::Model>)> {
|
||||
self.transaction(|tx| async move {
|
||||
let memberships = channel_member::Entity::find()
|
||||
.filter(channel_member::Column::UserId.eq(user_id))
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
let channels = self
|
||||
.get_channel_descendants_including_self(
|
||||
memberships.iter().map(|m| m.channel_id),
|
||||
&*tx,
|
||||
)
|
||||
.await?;
|
||||
Ok((memberships, channels))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns all channels for the user with the given ID.
|
||||
pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<ChannelsForUser> {
|
||||
self.transaction(|tx| async move {
|
||||
@@ -609,12 +580,16 @@ impl Database {
|
||||
ancestor_channel: Option<&channel::Model>,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<ChannelsForUser> {
|
||||
let mut filter = channel_member::Column::UserId
|
||||
.eq(user_id)
|
||||
.and(channel_member::Column::Accepted.eq(true));
|
||||
|
||||
if let Some(ancestor) = ancestor_channel {
|
||||
filter = filter.and(channel_member::Column::ChannelId.eq(ancestor.root_id()));
|
||||
}
|
||||
|
||||
let channel_memberships = channel_member::Entity::find()
|
||||
.filter(
|
||||
channel_member::Column::UserId
|
||||
.eq(user_id)
|
||||
.and(channel_member::Column::Accepted.eq(true)),
|
||||
)
|
||||
.filter(filter)
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
@@ -625,56 +600,20 @@ impl Database {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut roles_by_channel_id: HashMap<ChannelId, ChannelRole> = HashMap::default();
|
||||
for membership in channel_memberships.iter() {
|
||||
roles_by_channel_id.insert(membership.channel_id, membership.role);
|
||||
}
|
||||
|
||||
let mut visible_channel_ids: HashSet<ChannelId> = HashSet::default();
|
||||
let roles_by_channel_id = channel_memberships
|
||||
.iter()
|
||||
.map(|membership| (membership.channel_id, membership.role))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let channels: Vec<Channel> = descendants
|
||||
.into_iter()
|
||||
.filter_map(|channel| {
|
||||
let parent_role = channel
|
||||
.parent_id()
|
||||
.and_then(|parent_id| roles_by_channel_id.get(&parent_id));
|
||||
|
||||
let role = if let Some(parent_role) = parent_role {
|
||||
let role = if let Some(existing_role) = roles_by_channel_id.get(&channel.id) {
|
||||
existing_role.max(*parent_role)
|
||||
} else {
|
||||
*parent_role
|
||||
};
|
||||
roles_by_channel_id.insert(channel.id, role);
|
||||
role
|
||||
let parent_role = roles_by_channel_id.get(&channel.root_id())?;
|
||||
if parent_role.can_see_channel(channel.visibility) {
|
||||
Some(Channel::from_model(channel))
|
||||
} else {
|
||||
*roles_by_channel_id.get(&channel.id)?
|
||||
};
|
||||
|
||||
let can_see_parent_paths = role.can_see_all_descendants()
|
||||
|| role.can_only_see_public_descendants()
|
||||
&& channel.visibility == ChannelVisibility::Public;
|
||||
if !can_see_parent_paths {
|
||||
return None;
|
||||
None
|
||||
}
|
||||
|
||||
visible_channel_ids.insert(channel.id);
|
||||
|
||||
if let Some(ancestor) = ancestor_channel {
|
||||
if !channel
|
||||
.ancestors_including_self()
|
||||
.any(|id| id == ancestor.id)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let mut channel = Channel::from_model(channel, role);
|
||||
channel
|
||||
.parent_path
|
||||
.retain(|id| visible_channel_ids.contains(&id));
|
||||
|
||||
Some(channel)
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -702,76 +641,21 @@ impl Database {
|
||||
}
|
||||
|
||||
let channel_ids = channels.iter().map(|c| c.id).collect::<Vec<_>>();
|
||||
let channel_buffer_changes = self
|
||||
.unseen_channel_buffer_changes(user_id, &channel_ids, &*tx)
|
||||
let latest_buffer_versions = self
|
||||
.latest_channel_buffer_changes(&channel_ids, &*tx)
|
||||
.await?;
|
||||
|
||||
let unseen_messages = self
|
||||
.unseen_channel_messages(user_id, &channel_ids, &*tx)
|
||||
.await?;
|
||||
let latest_messages = self.latest_channel_messages(&channel_ids, &*tx).await?;
|
||||
|
||||
Ok(ChannelsForUser {
|
||||
channel_memberships,
|
||||
channels,
|
||||
channel_participants,
|
||||
unseen_buffer_changes: channel_buffer_changes,
|
||||
channel_messages: unseen_messages,
|
||||
latest_buffer_versions,
|
||||
latest_channel_messages: latest_messages,
|
||||
})
|
||||
}
|
||||
|
||||
async fn participants_to_notify_for_channel_change(
|
||||
&self,
|
||||
new_parent: &channel::Model,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Vec<(UserId, ChannelsForUser)>> {
|
||||
let mut results: Vec<(UserId, ChannelsForUser)> = Vec::new();
|
||||
|
||||
let members = self
|
||||
.get_channel_participant_details_internal(new_parent, &*tx)
|
||||
.await?;
|
||||
|
||||
for member in members.iter() {
|
||||
if !member.role.can_see_all_descendants() {
|
||||
continue;
|
||||
}
|
||||
results.push((
|
||||
member.user_id,
|
||||
self.get_user_channels(member.user_id, Some(new_parent), &*tx)
|
||||
.await?,
|
||||
))
|
||||
}
|
||||
|
||||
let public_parents = self
|
||||
.public_ancestors_including_self(new_parent, &*tx)
|
||||
.await?;
|
||||
let public_parent = public_parents.last();
|
||||
|
||||
let Some(public_parent) = public_parent else {
|
||||
return Ok(results);
|
||||
};
|
||||
|
||||
// could save some time in the common case by skipping this if the
|
||||
// new channel is not public and has no public descendants.
|
||||
let public_members = if public_parent == new_parent {
|
||||
members
|
||||
} else {
|
||||
self.get_channel_participant_details_internal(public_parent, &*tx)
|
||||
.await?
|
||||
};
|
||||
|
||||
for member in public_members {
|
||||
if !member.role.can_only_see_public_descendants() {
|
||||
continue;
|
||||
};
|
||||
results.push((
|
||||
member.user_id,
|
||||
self.get_user_channels(member.user_id, Some(public_parent), &*tx)
|
||||
.await?,
|
||||
))
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Sets the role for the specified channel member.
|
||||
pub async fn set_channel_member_role(
|
||||
&self,
|
||||
@@ -809,7 +693,7 @@ impl Database {
|
||||
))
|
||||
} else {
|
||||
Ok(SetMemberRoleResult::InviteUpdated(Channel::from_model(
|
||||
channel, role,
|
||||
channel,
|
||||
)))
|
||||
}
|
||||
})
|
||||
@@ -839,22 +723,30 @@ impl Database {
|
||||
if role == ChannelRole::Admin {
|
||||
Ok(members
|
||||
.into_iter()
|
||||
.map(|channel_member| channel_member.to_proto())
|
||||
.map(|channel_member| proto::ChannelMember {
|
||||
role: channel_member.role.into(),
|
||||
user_id: channel_member.user_id.to_proto(),
|
||||
kind: if channel_member.accepted {
|
||||
Kind::Member
|
||||
} else {
|
||||
Kind::Invitee
|
||||
}
|
||||
.into(),
|
||||
})
|
||||
.collect())
|
||||
} else {
|
||||
return Ok(members
|
||||
.into_iter()
|
||||
.filter_map(|member| {
|
||||
if member.kind == proto::channel_member::Kind::Invitee {
|
||||
if !member.accepted {
|
||||
return None;
|
||||
}
|
||||
Some(ChannelMember {
|
||||
role: member.role,
|
||||
user_id: member.user_id,
|
||||
kind: proto::channel_member::Kind::Member,
|
||||
Some(proto::ChannelMember {
|
||||
role: member.role.into(),
|
||||
user_id: member.user_id.to_proto(),
|
||||
kind: Kind::Member.into(),
|
||||
})
|
||||
})
|
||||
.map(|channel_member| channel_member.to_proto())
|
||||
.collect());
|
||||
}
|
||||
}
|
||||
@@ -863,83 +755,11 @@ impl Database {
|
||||
&self,
|
||||
channel: &channel::Model,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Vec<ChannelMember>> {
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
enum QueryMemberDetails {
|
||||
UserId,
|
||||
Role,
|
||||
IsDirectMember,
|
||||
Accepted,
|
||||
Visibility,
|
||||
}
|
||||
|
||||
let mut stream = channel_member::Entity::find()
|
||||
.left_join(channel::Entity)
|
||||
.filter(channel_member::Column::ChannelId.is_in(channel.ancestors_including_self()))
|
||||
.select_only()
|
||||
.column(channel_member::Column::UserId)
|
||||
.column(channel_member::Column::Role)
|
||||
.column_as(
|
||||
channel_member::Column::ChannelId.eq(channel.id),
|
||||
QueryMemberDetails::IsDirectMember,
|
||||
)
|
||||
.column(channel_member::Column::Accepted)
|
||||
.column(channel::Column::Visibility)
|
||||
.into_values::<_, QueryMemberDetails>()
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut user_details: HashMap<UserId, ChannelMember> = HashMap::default();
|
||||
|
||||
while let Some(user_membership) = stream.next().await {
|
||||
let (user_id, channel_role, is_direct_member, is_invite_accepted, visibility): (
|
||||
UserId,
|
||||
ChannelRole,
|
||||
bool,
|
||||
bool,
|
||||
ChannelVisibility,
|
||||
) = user_membership?;
|
||||
let kind = match (is_direct_member, is_invite_accepted) {
|
||||
(true, true) => proto::channel_member::Kind::Member,
|
||||
(true, false) => proto::channel_member::Kind::Invitee,
|
||||
(false, true) => proto::channel_member::Kind::AncestorMember,
|
||||
(false, false) => continue,
|
||||
};
|
||||
|
||||
if channel_role == ChannelRole::Guest
|
||||
&& visibility != ChannelVisibility::Public
|
||||
&& channel.visibility != ChannelVisibility::Public
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(details_mut) = user_details.get_mut(&user_id) {
|
||||
if channel_role.should_override(details_mut.role) {
|
||||
details_mut.role = channel_role;
|
||||
}
|
||||
if kind == Kind::Member {
|
||||
details_mut.kind = kind;
|
||||
// the UI is going to be a bit confusing if you already have permissions
|
||||
// that are greater than or equal to the ones you're being invited to.
|
||||
} else if kind == Kind::Invitee && details_mut.kind == Kind::AncestorMember {
|
||||
details_mut.kind = kind;
|
||||
}
|
||||
} else {
|
||||
user_details.insert(
|
||||
user_id,
|
||||
ChannelMember {
|
||||
user_id,
|
||||
kind,
|
||||
role: channel_role,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(user_details
|
||||
.into_iter()
|
||||
.map(|(_, details)| details)
|
||||
.collect())
|
||||
) -> Result<Vec<channel_member::Model>> {
|
||||
Ok(channel_member::Entity::find()
|
||||
.filter(channel_member::Column::ChannelId.eq(channel.root_id()))
|
||||
.all(tx)
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Returns the participants in the given channel.
|
||||
@@ -1018,7 +838,7 @@ impl Database {
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Option<channel_member::Model>> {
|
||||
let row = channel_member::Entity::find()
|
||||
.filter(channel_member::Column::ChannelId.is_in(channel.ancestors_including_self()))
|
||||
.filter(channel_member::Column::ChannelId.eq(channel.root_id()))
|
||||
.filter(channel_member::Column::UserId.eq(user_id))
|
||||
.filter(channel_member::Column::Accepted.eq(false))
|
||||
.one(&*tx)
|
||||
@@ -1027,33 +847,6 @@ impl Database {
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
async fn public_parent_channel(
|
||||
&self,
|
||||
channel: &channel::Model,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Option<channel::Model>> {
|
||||
let mut path = self.public_ancestors_including_self(channel, &*tx).await?;
|
||||
if path.last().unwrap().id == channel.id {
|
||||
path.pop();
|
||||
}
|
||||
Ok(path.pop())
|
||||
}
|
||||
|
||||
pub(crate) async fn public_ancestors_including_self(
|
||||
&self,
|
||||
channel: &channel::Model,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Vec<channel::Model>> {
|
||||
let visible_channels = channel::Entity::find()
|
||||
.filter(channel::Column::Id.is_in(channel.ancestors_including_self()))
|
||||
.filter(channel::Column::Visibility.eq(ChannelVisibility::Public))
|
||||
.order_by_asc(channel::Column::ParentPath)
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
Ok(visible_channels)
|
||||
}
|
||||
|
||||
/// Returns the role for a user in the given channel.
|
||||
pub async fn channel_role_for_user(
|
||||
&self,
|
||||
@@ -1061,77 +854,25 @@ impl Database {
|
||||
user_id: UserId,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Option<ChannelRole>> {
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
enum QueryChannelMembership {
|
||||
ChannelId,
|
||||
Role,
|
||||
Visibility,
|
||||
}
|
||||
|
||||
let mut rows = channel_member::Entity::find()
|
||||
.left_join(channel::Entity)
|
||||
let membership = channel_member::Entity::find()
|
||||
.filter(
|
||||
channel_member::Column::ChannelId
|
||||
.is_in(channel.ancestors_including_self())
|
||||
.eq(channel.root_id())
|
||||
.and(channel_member::Column::UserId.eq(user_id))
|
||||
.and(channel_member::Column::Accepted.eq(true)),
|
||||
)
|
||||
.select_only()
|
||||
.column(channel_member::Column::ChannelId)
|
||||
.column(channel_member::Column::Role)
|
||||
.column(channel::Column::Visibility)
|
||||
.into_values::<_, QueryChannelMembership>()
|
||||
.stream(&*tx)
|
||||
.one(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut user_role: Option<ChannelRole> = None;
|
||||
let Some(membership) = membership else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut is_participant = false;
|
||||
let mut current_channel_visibility = None;
|
||||
|
||||
// note these channels are not iterated in any particular order,
|
||||
// our current logic takes the highest permission available.
|
||||
while let Some(row) = rows.next().await {
|
||||
let (membership_channel, role, visibility): (
|
||||
ChannelId,
|
||||
ChannelRole,
|
||||
ChannelVisibility,
|
||||
) = row?;
|
||||
|
||||
match role {
|
||||
ChannelRole::Admin | ChannelRole::Member | ChannelRole::Banned => {
|
||||
if let Some(users_role) = user_role {
|
||||
user_role = Some(users_role.max(role));
|
||||
} else {
|
||||
user_role = Some(role)
|
||||
}
|
||||
}
|
||||
ChannelRole::Guest if visibility == ChannelVisibility::Public => {
|
||||
is_participant = true
|
||||
}
|
||||
ChannelRole::Guest => {}
|
||||
}
|
||||
if channel.id == membership_channel {
|
||||
current_channel_visibility = Some(visibility);
|
||||
}
|
||||
}
|
||||
// free up database connection
|
||||
drop(rows);
|
||||
|
||||
if is_participant && user_role.is_none() {
|
||||
if current_channel_visibility.is_none() {
|
||||
current_channel_visibility = channel::Entity::find()
|
||||
.filter(channel::Column::Id.eq(channel.id))
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.map(|channel| channel.visibility);
|
||||
}
|
||||
if current_channel_visibility == Some(ChannelVisibility::Public) {
|
||||
user_role = Some(ChannelRole::Guest);
|
||||
}
|
||||
if !membership.role.can_see_channel(channel.visibility) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(user_role)
|
||||
Ok(Some(membership.role))
|
||||
}
|
||||
|
||||
// Get the descendants of the given set if channels, ordered by their
|
||||
@@ -1184,11 +925,10 @@ impl Database {
|
||||
pub async fn get_channel(&self, channel_id: ChannelId, user_id: UserId) -> Result<Channel> {
|
||||
self.transaction(|tx| async move {
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
let role = self
|
||||
.check_user_is_channel_participant(&channel, user_id, &*tx)
|
||||
self.check_user_is_channel_participant(&channel, user_id, &*tx)
|
||||
.await?;
|
||||
|
||||
Ok(Channel::from_model(channel, role))
|
||||
Ok(Channel::from_model(channel))
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -1201,7 +941,7 @@ impl Database {
|
||||
Ok(channel::Entity::find_by_id(channel_id)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such channel"))?)
|
||||
.ok_or_else(|| proto::ErrorCode::NoSuchChannel.anyhow())?)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_or_create_channel_room(
|
||||
@@ -1219,7 +959,9 @@ impl Database {
|
||||
let room_id = if let Some(room) = room {
|
||||
if let Some(env) = room.environment {
|
||||
if &env != environment {
|
||||
Err(anyhow!("must join using the {} release", env))?;
|
||||
Err(ErrorCode::WrongReleaseChannel
|
||||
.with_tag("required", &env)
|
||||
.anyhow())?;
|
||||
}
|
||||
}
|
||||
room.id
|
||||
@@ -1243,62 +985,40 @@ impl Database {
|
||||
pub async fn move_channel(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
new_parent_id: Option<ChannelId>,
|
||||
new_parent_id: ChannelId,
|
||||
admin_id: UserId,
|
||||
) -> Result<Option<MoveChannelResult>> {
|
||||
) -> Result<(Vec<Channel>, Vec<channel_member::Model>)> {
|
||||
self.transaction(|tx| async move {
|
||||
let channel = self.get_channel_internal(channel_id, &*tx).await?;
|
||||
self.check_user_is_channel_admin(&channel, admin_id, &*tx)
|
||||
.await?;
|
||||
let new_parent = self.get_channel_internal(new_parent_id, &*tx).await?;
|
||||
|
||||
let new_parent_path;
|
||||
let new_parent_channel;
|
||||
if let Some(new_parent_id) = new_parent_id {
|
||||
let new_parent = self.get_channel_internal(new_parent_id, &*tx).await?;
|
||||
self.check_user_is_channel_admin(&new_parent, admin_id, &*tx)
|
||||
.await?;
|
||||
|
||||
if new_parent
|
||||
.ancestors_including_self()
|
||||
.any(|id| id == channel.id)
|
||||
{
|
||||
Err(anyhow!("cannot move a channel into one of its descendants"))?;
|
||||
}
|
||||
|
||||
new_parent_path = new_parent.path();
|
||||
new_parent_channel = Some(new_parent);
|
||||
} else {
|
||||
new_parent_path = String::new();
|
||||
new_parent_channel = None;
|
||||
};
|
||||
|
||||
let previous_participants = self
|
||||
.get_channel_participant_details_internal(&channel, &*tx)
|
||||
.await?;
|
||||
|
||||
let old_path = format!("{}{}/", channel.parent_path, channel.id);
|
||||
let new_path = format!("{}{}/", new_parent_path, channel.id);
|
||||
|
||||
if old_path == new_path {
|
||||
return Ok(None);
|
||||
if new_parent.root_id() != channel.root_id() {
|
||||
Err(anyhow!(ErrorCode::WrongMoveTarget))?;
|
||||
}
|
||||
|
||||
if new_parent
|
||||
.ancestors_including_self()
|
||||
.any(|id| id == channel.id)
|
||||
{
|
||||
Err(anyhow!(ErrorCode::CircularNesting))?;
|
||||
}
|
||||
|
||||
if channel.visibility == ChannelVisibility::Public
|
||||
&& new_parent.visibility != ChannelVisibility::Public
|
||||
{
|
||||
Err(anyhow!(ErrorCode::BadPublicNesting))?;
|
||||
}
|
||||
|
||||
let root_id = channel.root_id();
|
||||
let old_path = format!("{}{}/", channel.parent_path, channel.id);
|
||||
let new_path = format!("{}{}/", new_parent.path(), channel.id);
|
||||
|
||||
let mut model = channel.into_active_model();
|
||||
model.parent_path = ActiveValue::Set(new_parent_path);
|
||||
model.parent_path = ActiveValue::Set(new_parent.path());
|
||||
let channel = model.update(&*tx).await?;
|
||||
|
||||
if new_parent_channel.is_none() {
|
||||
channel_member::ActiveModel {
|
||||
id: ActiveValue::NotSet,
|
||||
channel_id: ActiveValue::Set(channel_id),
|
||||
user_id: ActiveValue::Set(admin_id),
|
||||
accepted: ActiveValue::Set(true),
|
||||
role: ActiveValue::Set(ChannelRole::Admin),
|
||||
}
|
||||
.insert(&*tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let descendent_ids =
|
||||
ChannelId::find_by_statement::<QueryIds>(Statement::from_sql_and_values(
|
||||
self.pool.get_database_backend(),
|
||||
@@ -1312,35 +1032,22 @@ impl Database {
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
let participants_to_update: HashMap<_, _> = self
|
||||
.participants_to_notify_for_channel_change(
|
||||
new_parent_channel.as_ref().unwrap_or(&channel),
|
||||
&*tx,
|
||||
)
|
||||
let all_moved_ids = Some(channel.id).into_iter().chain(descendent_ids);
|
||||
|
||||
let channels = channel::Entity::find()
|
||||
.filter(channel::Column::Id.is_in(all_moved_ids))
|
||||
.all(&*tx)
|
||||
.await?
|
||||
.into_iter()
|
||||
.collect();
|
||||
.map(|c| Channel::from_model(c))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut moved_channels: HashSet<ChannelId> = HashSet::default();
|
||||
for id in descendent_ids {
|
||||
moved_channels.insert(id);
|
||||
}
|
||||
moved_channels.insert(channel_id);
|
||||
let channel_members = channel_member::Entity::find()
|
||||
.filter(channel_member::Column::ChannelId.eq(root_id))
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut participants_to_remove: HashSet<UserId> = HashSet::default();
|
||||
for participant in previous_participants {
|
||||
if participant.kind == proto::channel_member::Kind::AncestorMember {
|
||||
if !participants_to_update.contains_key(&participant.user_id) {
|
||||
participants_to_remove.insert(participant.user_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Some(MoveChannelResult {
|
||||
participants_to_remove,
|
||||
participants_to_update,
|
||||
moved_channels,
|
||||
}))
|
||||
Ok((channels, channel_members))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -385,25 +385,11 @@ impl Database {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the unseen messages for the given user in the specified channels.
|
||||
pub async fn unseen_channel_messages(
|
||||
pub async fn latest_channel_messages(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
channel_ids: &[ChannelId],
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Vec<proto::UnseenChannelMessage>> {
|
||||
let mut observed_messages_by_channel_id = HashMap::default();
|
||||
let mut rows = observed_channel_messages::Entity::find()
|
||||
.filter(observed_channel_messages::Column::UserId.eq(user_id))
|
||||
.filter(observed_channel_messages::Column::ChannelId.is_in(channel_ids.iter().copied()))
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
while let Some(row) = rows.next().await {
|
||||
let row = row?;
|
||||
observed_messages_by_channel_id.insert(row.channel_id, row);
|
||||
}
|
||||
drop(rows);
|
||||
) -> Result<Vec<proto::ChannelMessageId>> {
|
||||
let mut values = String::new();
|
||||
for id in channel_ids {
|
||||
if !values.is_empty() {
|
||||
@@ -413,7 +399,7 @@ impl Database {
|
||||
}
|
||||
|
||||
if values.is_empty() {
|
||||
return Ok(Default::default());
|
||||
return Ok(Vec::default());
|
||||
}
|
||||
|
||||
let sql = format!(
|
||||
@@ -437,26 +423,20 @@ impl Database {
|
||||
);
|
||||
|
||||
let stmt = Statement::from_string(self.pool.get_database_backend(), sql);
|
||||
let last_messages = channel_message::Model::find_by_statement(stmt)
|
||||
.all(&*tx)
|
||||
let mut last_messages = channel_message::Model::find_by_statement(stmt)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut changes = Vec::new();
|
||||
for last_message in last_messages {
|
||||
if let Some(observed_message) =
|
||||
observed_messages_by_channel_id.get(&last_message.channel_id)
|
||||
{
|
||||
if observed_message.channel_message_id == last_message.id {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
changes.push(proto::UnseenChannelMessage {
|
||||
channel_id: last_message.channel_id.to_proto(),
|
||||
message_id: last_message.id.to_proto(),
|
||||
let mut results = Vec::new();
|
||||
while let Some(result) = last_messages.next().await {
|
||||
let message = result?;
|
||||
results.push(proto::ChannelMessageId {
|
||||
channel_id: message.channel_id.to_proto(),
|
||||
message_id: message.id.to_proto(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(changes)
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Removes the channel message with the given ID.
|
||||
|
||||
@@ -17,6 +17,14 @@ impl Model {
|
||||
self.ancestors().last()
|
||||
}
|
||||
|
||||
pub fn is_root(&self) -> bool {
|
||||
self.parent_path.is_empty()
|
||||
}
|
||||
|
||||
pub fn root_id(&self) -> ChannelId {
|
||||
self.ancestors().next().unwrap_or(self.id)
|
||||
}
|
||||
|
||||
pub fn ancestors(&self) -> impl Iterator<Item = ChannelId> + '_ {
|
||||
self.parent_path
|
||||
.trim_end_matches('/')
|
||||
|
||||
@@ -150,14 +150,13 @@ impl Drop for TestDb {
|
||||
}
|
||||
}
|
||||
|
||||
fn channel_tree(channels: &[(ChannelId, &[ChannelId], &'static str, ChannelRole)]) -> Vec<Channel> {
|
||||
fn channel_tree(channels: &[(ChannelId, &[ChannelId], &'static str)]) -> Vec<Channel> {
|
||||
channels
|
||||
.iter()
|
||||
.map(|(id, parent_path, name, role)| Channel {
|
||||
.map(|(id, parent_path, name)| Channel {
|
||||
id: *id,
|
||||
name: name.to_string(),
|
||||
visibility: ChannelVisibility::Members,
|
||||
role: *role,
|
||||
parent_path: parent_path.to_vec(),
|
||||
})
|
||||
.collect()
|
||||
|
||||
@@ -330,8 +330,7 @@ async fn test_channel_buffers_last_operations(db: &Database) {
|
||||
.transaction(|tx| {
|
||||
let buffers = &buffers;
|
||||
async move {
|
||||
db.unseen_channel_buffer_changes(
|
||||
observer_id,
|
||||
db.latest_channel_buffer_changes(
|
||||
&[
|
||||
buffers[0].channel_id,
|
||||
buffers[1].channel_id,
|
||||
@@ -348,12 +347,12 @@ async fn test_channel_buffers_last_operations(db: &Database) {
|
||||
pretty_assertions::assert_eq!(
|
||||
buffer_changes,
|
||||
[
|
||||
rpc::proto::UnseenChannelBufferChange {
|
||||
rpc::proto::ChannelBufferVersion {
|
||||
channel_id: buffers[0].channel_id.to_proto(),
|
||||
epoch: 0,
|
||||
version: serialize_version(&text_buffers[0].version()),
|
||||
},
|
||||
rpc::proto::UnseenChannelBufferChange {
|
||||
rpc::proto::ChannelBufferVersion {
|
||||
channel_id: buffers[1].channel_id.to_proto(),
|
||||
epoch: 1,
|
||||
version: serialize_version(&text_buffers[1].version())
|
||||
@@ -362,99 +361,7 @@ async fn test_channel_buffers_last_operations(db: &Database) {
|
||||
== buffer_changes[1].version.first().unwrap().replica_id)
|
||||
.collect::<Vec<_>>(),
|
||||
},
|
||||
rpc::proto::UnseenChannelBufferChange {
|
||||
channel_id: buffers[2].channel_id.to_proto(),
|
||||
epoch: 0,
|
||||
version: serialize_version(&text_buffers[2].version()),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
db.observe_buffer_version(
|
||||
buffers[1].id,
|
||||
observer_id,
|
||||
1,
|
||||
serialize_version(&text_buffers[1].version()).as_slice(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let buffer_changes = db
|
||||
.transaction(|tx| {
|
||||
let buffers = &buffers;
|
||||
async move {
|
||||
db.unseen_channel_buffer_changes(
|
||||
observer_id,
|
||||
&[
|
||||
buffers[0].channel_id,
|
||||
buffers[1].channel_id,
|
||||
buffers[2].channel_id,
|
||||
],
|
||||
&*tx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
buffer_changes,
|
||||
[
|
||||
rpc::proto::UnseenChannelBufferChange {
|
||||
channel_id: buffers[0].channel_id.to_proto(),
|
||||
epoch: 0,
|
||||
version: serialize_version(&text_buffers[0].version()),
|
||||
},
|
||||
rpc::proto::UnseenChannelBufferChange {
|
||||
channel_id: buffers[2].channel_id.to_proto(),
|
||||
epoch: 0,
|
||||
version: serialize_version(&text_buffers[2].version()),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Observe an earlier version of the buffer.
|
||||
db.observe_buffer_version(
|
||||
buffers[1].id,
|
||||
observer_id,
|
||||
1,
|
||||
&[rpc::proto::VectorClockEntry {
|
||||
replica_id: 0,
|
||||
timestamp: 0,
|
||||
}],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let buffer_changes = db
|
||||
.transaction(|tx| {
|
||||
let buffers = &buffers;
|
||||
async move {
|
||||
db.unseen_channel_buffer_changes(
|
||||
observer_id,
|
||||
&[
|
||||
buffers[0].channel_id,
|
||||
buffers[1].channel_id,
|
||||
buffers[2].channel_id,
|
||||
],
|
||||
&*tx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
buffer_changes,
|
||||
[
|
||||
rpc::proto::UnseenChannelBufferChange {
|
||||
channel_id: buffers[0].channel_id.to_proto(),
|
||||
epoch: 0,
|
||||
version: serialize_version(&text_buffers[0].version()),
|
||||
},
|
||||
rpc::proto::UnseenChannelBufferChange {
|
||||
rpc::proto::ChannelBufferVersion {
|
||||
channel_id: buffers[2].channel_id.to_proto(),
|
||||
epoch: 0,
|
||||
version: serialize_version(&text_buffers[2].version()),
|
||||
|
||||
@@ -62,23 +62,13 @@ async fn test_channels(db: &Arc<Database>) {
|
||||
assert_eq!(
|
||||
result.channels,
|
||||
channel_tree(&[
|
||||
(zed_id, &[], "zed", ChannelRole::Admin),
|
||||
(crdb_id, &[zed_id], "crdb", ChannelRole::Admin),
|
||||
(
|
||||
livestreaming_id,
|
||||
&[zed_id],
|
||||
"livestreaming",
|
||||
ChannelRole::Admin
|
||||
),
|
||||
(replace_id, &[zed_id], "replace", ChannelRole::Admin),
|
||||
(rust_id, &[], "rust", ChannelRole::Admin),
|
||||
(cargo_id, &[rust_id], "cargo", ChannelRole::Admin),
|
||||
(
|
||||
cargo_ra_id,
|
||||
&[rust_id, cargo_id],
|
||||
"cargo-ra",
|
||||
ChannelRole::Admin
|
||||
)
|
||||
(zed_id, &[], "zed"),
|
||||
(crdb_id, &[zed_id], "crdb"),
|
||||
(livestreaming_id, &[zed_id], "livestreaming",),
|
||||
(replace_id, &[zed_id], "replace"),
|
||||
(rust_id, &[], "rust"),
|
||||
(cargo_id, &[rust_id], "cargo"),
|
||||
(cargo_ra_id, &[rust_id, cargo_id], "cargo-ra",)
|
||||
],)
|
||||
);
|
||||
|
||||
@@ -86,15 +76,10 @@ async fn test_channels(db: &Arc<Database>) {
|
||||
assert_eq!(
|
||||
result.channels,
|
||||
channel_tree(&[
|
||||
(zed_id, &[], "zed", ChannelRole::Member),
|
||||
(crdb_id, &[zed_id], "crdb", ChannelRole::Member),
|
||||
(
|
||||
livestreaming_id,
|
||||
&[zed_id],
|
||||
"livestreaming",
|
||||
ChannelRole::Member
|
||||
),
|
||||
(replace_id, &[zed_id], "replace", ChannelRole::Member)
|
||||
(zed_id, &[], "zed"),
|
||||
(crdb_id, &[zed_id], "crdb"),
|
||||
(livestreaming_id, &[zed_id], "livestreaming",),
|
||||
(replace_id, &[zed_id], "replace")
|
||||
],)
|
||||
);
|
||||
|
||||
@@ -112,15 +97,10 @@ async fn test_channels(db: &Arc<Database>) {
|
||||
assert_eq!(
|
||||
result.channels,
|
||||
channel_tree(&[
|
||||
(zed_id, &[], "zed", ChannelRole::Admin),
|
||||
(crdb_id, &[zed_id], "crdb", ChannelRole::Admin),
|
||||
(
|
||||
livestreaming_id,
|
||||
&[zed_id],
|
||||
"livestreaming",
|
||||
ChannelRole::Admin
|
||||
),
|
||||
(replace_id, &[zed_id], "replace", ChannelRole::Admin)
|
||||
(zed_id, &[], "zed"),
|
||||
(crdb_id, &[zed_id], "crdb"),
|
||||
(livestreaming_id, &[zed_id], "livestreaming",),
|
||||
(replace_id, &[zed_id], "replace")
|
||||
],)
|
||||
);
|
||||
|
||||
@@ -271,14 +251,19 @@ async fn test_channel_invites(db: &Arc<Database>) {
|
||||
&[
|
||||
proto::ChannelMember {
|
||||
user_id: user_1.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: user_2.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: user_3.to_proto(),
|
||||
kind: proto::channel_member::Kind::Invitee.into(),
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -420,13 +405,6 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Move to same parent should be a no-op
|
||||
assert!(db
|
||||
.move_channel(projects_id, Some(zed_id), user_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none());
|
||||
|
||||
let result = db.get_channels_for_user(user_id).await.unwrap();
|
||||
assert_channel_tree(
|
||||
result.channels,
|
||||
@@ -437,20 +415,8 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
|
||||
],
|
||||
);
|
||||
|
||||
// Move the project channel to the root
|
||||
db.move_channel(projects_id, None, user_id).await.unwrap();
|
||||
let result = db.get_channels_for_user(user_id).await.unwrap();
|
||||
assert_channel_tree(
|
||||
result.channels,
|
||||
&[
|
||||
(zed_id, &[]),
|
||||
(projects_id, &[]),
|
||||
(livestreaming_id, &[projects_id]),
|
||||
],
|
||||
);
|
||||
|
||||
// Can't move a channel into its ancestor
|
||||
db.move_channel(projects_id, Some(livestreaming_id), user_id)
|
||||
db.move_channel(projects_id, livestreaming_id, user_id)
|
||||
.await
|
||||
.unwrap_err();
|
||||
let result = db.get_channels_for_user(user_id).await.unwrap();
|
||||
@@ -458,8 +424,8 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
|
||||
result.channels,
|
||||
&[
|
||||
(zed_id, &[]),
|
||||
(projects_id, &[]),
|
||||
(livestreaming_id, &[projects_id]),
|
||||
(projects_id, &[zed_id]),
|
||||
(livestreaming_id, &[zed_id, projects_id]),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -476,32 +442,39 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
let guest = new_test_user(db, "guest@example.com").await;
|
||||
|
||||
let zed_channel = db.create_root_channel("zed", admin).await.unwrap();
|
||||
let active_channel_id = db
|
||||
let internal_channel_id = db
|
||||
.create_sub_channel("active", zed_channel, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
let vim_channel_id = db
|
||||
.create_sub_channel("vim", active_channel_id, admin)
|
||||
let public_channel_id = db
|
||||
.create_sub_channel("vim", zed_channel, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.set_channel_visibility(vim_channel_id, crate::db::ChannelVisibility::Public, admin)
|
||||
db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
db.invite_channel_member(active_channel_id, member, admin, ChannelRole::Member)
|
||||
db.set_channel_visibility(
|
||||
public_channel_id,
|
||||
crate::db::ChannelVisibility::Public,
|
||||
admin,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
db.invite_channel_member(zed_channel, member, admin, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
db.invite_channel_member(vim_channel_id, guest, admin, ChannelRole::Guest)
|
||||
db.invite_channel_member(zed_channel, guest, admin, ChannelRole::Guest)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.respond_to_channel_invite(active_channel_id, member, true)
|
||||
db.respond_to_channel_invite(zed_channel, member, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(
|
||||
&db.get_channel_internal(vim_channel_id, &*tx).await?,
|
||||
&db.get_channel_internal(public_channel_id, &*tx).await?,
|
||||
admin,
|
||||
&*tx,
|
||||
)
|
||||
@@ -511,7 +484,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
.unwrap();
|
||||
db.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(
|
||||
&db.get_channel_internal(vim_channel_id, &*tx).await?,
|
||||
&db.get_channel_internal(public_channel_id, &*tx).await?,
|
||||
member,
|
||||
&*tx,
|
||||
)
|
||||
@@ -521,7 +494,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
.unwrap();
|
||||
|
||||
let mut members = db
|
||||
.get_channel_participant_details(vim_channel_id, admin)
|
||||
.get_channel_participant_details(public_channel_id, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -532,12 +505,12 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
&[
|
||||
proto::ChannelMember {
|
||||
user_id: admin.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: member.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
@@ -548,13 +521,13 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
]
|
||||
);
|
||||
|
||||
db.respond_to_channel_invite(vim_channel_id, guest, true)
|
||||
db.respond_to_channel_invite(zed_channel, guest, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(
|
||||
&db.get_channel_internal(vim_channel_id, &*tx).await?,
|
||||
&db.get_channel_internal(public_channel_id, &*tx).await?,
|
||||
guest,
|
||||
&*tx,
|
||||
)
|
||||
@@ -564,23 +537,29 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
.unwrap();
|
||||
|
||||
let channels = db.get_channels_for_user(guest).await.unwrap().channels;
|
||||
assert_channel_tree(channels, &[(vim_channel_id, &[])]);
|
||||
assert_channel_tree(
|
||||
channels,
|
||||
&[(zed_channel, &[]), (public_channel_id, &[zed_channel])],
|
||||
);
|
||||
let channels = db.get_channels_for_user(member).await.unwrap().channels;
|
||||
assert_channel_tree(
|
||||
channels,
|
||||
&[
|
||||
(active_channel_id, &[]),
|
||||
(vim_channel_id, &[active_channel_id]),
|
||||
(zed_channel, &[]),
|
||||
(internal_channel_id, &[zed_channel]),
|
||||
(public_channel_id, &[zed_channel]),
|
||||
],
|
||||
);
|
||||
|
||||
db.set_channel_member_role(vim_channel_id, admin, guest, ChannelRole::Banned)
|
||||
db.set_channel_member_role(zed_channel, admin, guest, ChannelRole::Banned)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(db
|
||||
.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(
|
||||
&db.get_channel_internal(vim_channel_id, &*tx).await.unwrap(),
|
||||
&db.get_channel_internal(public_channel_id, &*tx)
|
||||
.await
|
||||
.unwrap(),
|
||||
guest,
|
||||
&*tx,
|
||||
)
|
||||
@@ -590,7 +569,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
.is_err());
|
||||
|
||||
let mut members = db
|
||||
.get_channel_participant_details(vim_channel_id, admin)
|
||||
.get_channel_participant_details(public_channel_id, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -601,12 +580,12 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
&[
|
||||
proto::ChannelMember {
|
||||
user_id: admin.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: member.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
@@ -617,11 +596,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
]
|
||||
);
|
||||
|
||||
db.remove_channel_member(vim_channel_id, guest, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin)
|
||||
db.remove_channel_member(zed_channel, guest, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -631,7 +606,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
|
||||
// currently people invited to parent channels are not shown here
|
||||
let mut members = db
|
||||
.get_channel_participant_details(vim_channel_id, admin)
|
||||
.get_channel_participant_details(public_channel_id, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -642,14 +617,19 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
&[
|
||||
proto::ChannelMember {
|
||||
user_id: admin.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: member.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: guest.to_proto(),
|
||||
kind: proto::channel_member::Kind::Invitee.into(),
|
||||
role: proto::ChannelRole::Guest.into(),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
@@ -670,7 +650,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
assert!(db
|
||||
.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(
|
||||
&db.get_channel_internal(active_channel_id, &*tx)
|
||||
&db.get_channel_internal(internal_channel_id, &*tx)
|
||||
.await
|
||||
.unwrap(),
|
||||
guest,
|
||||
@@ -683,7 +663,9 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
|
||||
db.transaction(|tx| async move {
|
||||
db.check_user_is_channel_participant(
|
||||
&db.get_channel_internal(vim_channel_id, &*tx).await.unwrap(),
|
||||
&db.get_channel_internal(public_channel_id, &*tx)
|
||||
.await
|
||||
.unwrap(),
|
||||
guest,
|
||||
&*tx,
|
||||
)
|
||||
@@ -693,7 +675,7 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
.unwrap();
|
||||
|
||||
let mut members = db
|
||||
.get_channel_participant_details(vim_channel_id, admin)
|
||||
.get_channel_participant_details(public_channel_id, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -704,17 +686,17 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
&[
|
||||
proto::ChannelMember {
|
||||
user_id: admin.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Admin.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: member.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Member.into(),
|
||||
},
|
||||
proto::ChannelMember {
|
||||
user_id: guest.to_proto(),
|
||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||
kind: proto::channel_member::Kind::Member.into(),
|
||||
role: proto::ChannelRole::Guest.into(),
|
||||
},
|
||||
]
|
||||
@@ -723,67 +705,10 @@ async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||
let channels = db.get_channels_for_user(guest).await.unwrap().channels;
|
||||
assert_channel_tree(
|
||||
channels,
|
||||
&[(zed_channel, &[]), (vim_channel_id, &[zed_channel])],
|
||||
&[(zed_channel, &[]), (public_channel_id, &[zed_channel])],
|
||||
)
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
test_user_joins_correct_channel,
|
||||
test_user_joins_correct_channel_postgres,
|
||||
test_user_joins_correct_channel_sqlite
|
||||
);
|
||||
|
||||
async fn test_user_joins_correct_channel(db: &Arc<Database>) {
|
||||
let admin = new_test_user(db, "admin@example.com").await;
|
||||
|
||||
let zed_channel = db.create_root_channel("zed", admin).await.unwrap();
|
||||
|
||||
let active_channel = db
|
||||
.create_sub_channel("active", zed_channel, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let vim_channel = db
|
||||
.create_sub_channel("vim", active_channel, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let vim2_channel = db
|
||||
.create_sub_channel("vim2", vim_channel, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.set_channel_visibility(vim_channel, crate::db::ChannelVisibility::Public, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.set_channel_visibility(vim2_channel, crate::db::ChannelVisibility::Public, admin)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let most_public = db
|
||||
.transaction(|tx| async move {
|
||||
Ok(db
|
||||
.public_ancestors_including_self(
|
||||
&db.get_channel_internal(vim_channel, &*tx).await.unwrap(),
|
||||
&tx,
|
||||
)
|
||||
.await?
|
||||
.first()
|
||||
.cloned())
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.id;
|
||||
|
||||
assert_eq!(most_public, zed_channel)
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
test_guest_access,
|
||||
test_guest_access_postgres,
|
||||
|
||||
@@ -15,22 +15,18 @@ test_both_dbs!(
|
||||
|
||||
async fn test_channel_message_retrieval(db: &Arc<Database>) {
|
||||
let user = new_test_user(db, "user@example.com").await;
|
||||
let result = db.create_channel("channel", None, user).await.unwrap();
|
||||
let channel = db.create_channel("channel", None, user).await.unwrap().0;
|
||||
|
||||
let owner_id = db.create_server("test").await.unwrap().0 as u32;
|
||||
db.join_channel_chat(
|
||||
result.channel.id,
|
||||
rpc::ConnectionId { owner_id, id: 0 },
|
||||
user,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
db.join_channel_chat(channel.id, rpc::ConnectionId { owner_id, id: 0 }, user)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut all_messages = Vec::new();
|
||||
for i in 0..10 {
|
||||
all_messages.push(
|
||||
db.create_channel_message(
|
||||
result.channel.id,
|
||||
channel.id,
|
||||
user,
|
||||
&i.to_string(),
|
||||
&[],
|
||||
@@ -45,7 +41,7 @@ async fn test_channel_message_retrieval(db: &Arc<Database>) {
|
||||
}
|
||||
|
||||
let messages = db
|
||||
.get_channel_messages(result.channel.id, user, 3, None)
|
||||
.get_channel_messages(channel.id, user, 3, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
@@ -55,7 +51,7 @@ async fn test_channel_message_retrieval(db: &Arc<Database>) {
|
||||
|
||||
let messages = db
|
||||
.get_channel_messages(
|
||||
result.channel.id,
|
||||
channel.id,
|
||||
user,
|
||||
4,
|
||||
Some(MessageId::from_proto(all_messages[6])),
|
||||
@@ -239,11 +235,10 @@ async fn test_unseen_channel_messages(db: &Arc<Database>) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let second_message = db
|
||||
let _ = db
|
||||
.create_channel_message(channel_1, user, "1_2", &[], OffsetDateTime::now_utc(), 2)
|
||||
.await
|
||||
.unwrap()
|
||||
.message_id;
|
||||
.unwrap();
|
||||
|
||||
let third_message = db
|
||||
.create_channel_message(channel_1, user, "1_3", &[], OffsetDateTime::now_utc(), 3)
|
||||
@@ -262,97 +257,27 @@ async fn test_unseen_channel_messages(db: &Arc<Database>) {
|
||||
.message_id;
|
||||
|
||||
// Check that observer has new messages
|
||||
let unseen_messages = db
|
||||
let latest_messages = db
|
||||
.transaction(|tx| async move {
|
||||
db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx)
|
||||
db.latest_channel_messages(&[channel_1, channel_2], &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
unseen_messages,
|
||||
latest_messages,
|
||||
[
|
||||
rpc::proto::UnseenChannelMessage {
|
||||
rpc::proto::ChannelMessageId {
|
||||
channel_id: channel_1.to_proto(),
|
||||
message_id: third_message.to_proto(),
|
||||
},
|
||||
rpc::proto::UnseenChannelMessage {
|
||||
rpc::proto::ChannelMessageId {
|
||||
channel_id: channel_2.to_proto(),
|
||||
message_id: fourth_message.to_proto(),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Observe the second message
|
||||
db.observe_channel_message(channel_1, observer, second_message)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Make sure the observer still has a new message
|
||||
let unseen_messages = db
|
||||
.transaction(|tx| async move {
|
||||
db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
unseen_messages,
|
||||
[
|
||||
rpc::proto::UnseenChannelMessage {
|
||||
channel_id: channel_1.to_proto(),
|
||||
message_id: third_message.to_proto(),
|
||||
},
|
||||
rpc::proto::UnseenChannelMessage {
|
||||
channel_id: channel_2.to_proto(),
|
||||
message_id: fourth_message.to_proto(),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Observe the third message,
|
||||
db.observe_channel_message(channel_1, observer, third_message)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Make sure the observer does not have a new method
|
||||
let unseen_messages = db
|
||||
.transaction(|tx| async move {
|
||||
db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
unseen_messages,
|
||||
[rpc::proto::UnseenChannelMessage {
|
||||
channel_id: channel_2.to_proto(),
|
||||
message_id: fourth_message.to_proto(),
|
||||
}]
|
||||
);
|
||||
|
||||
// Observe the second message again, should not regress our observed state
|
||||
db.observe_channel_message(channel_1, observer, second_message)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Make sure the observer does not have a new message
|
||||
let unseen_messages = db
|
||||
.transaction(|tx| async move {
|
||||
db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
unseen_messages,
|
||||
[rpc::proto::UnseenChannelMessage {
|
||||
channel_id: channel_2.to_proto(),
|
||||
message_id: fourth_message.to_proto(),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
@@ -370,7 +295,7 @@ async fn test_channel_message_mentions(db: &Arc<Database>) {
|
||||
.create_channel("channel", None, user_a)
|
||||
.await
|
||||
.unwrap()
|
||||
.channel
|
||||
.0
|
||||
.id;
|
||||
db.invite_channel_member(channel, user_b, user_a, ChannelRole::Member)
|
||||
.await
|
||||
|
||||
@@ -3,14 +3,12 @@ mod connection_pool;
|
||||
use crate::{
|
||||
auth::{self, Impersonator},
|
||||
db::{
|
||||
self, BufferId, ChannelId, ChannelRole, ChannelsForUser, CreateChannelResult,
|
||||
CreatedChannelMessage, Database, InviteMemberResult, MembershipUpdated, MessageId,
|
||||
MoveChannelResult, NotificationId, ProjectId, RemoveChannelMemberResult,
|
||||
RenameChannelResult, RespondToChannelInvite, RoomId, ServerId, SetChannelVisibilityResult,
|
||||
User, UserId,
|
||||
self, BufferId, ChannelId, ChannelRole, ChannelsForUser, CreatedChannelMessage, Database,
|
||||
InviteMemberResult, MembershipUpdated, MessageId, NotificationId, ProjectId,
|
||||
RemoveChannelMemberResult, RespondToChannelInvite, RoomId, ServerId, User, UserId,
|
||||
},
|
||||
executor::Executor,
|
||||
AppState, Result,
|
||||
AppState, Error, Result,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use async_tungstenite::tungstenite::{
|
||||
@@ -44,7 +42,7 @@ use rpc::{
|
||||
self, Ack, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo,
|
||||
RequestMessage, ShareProject, UpdateChannelBufferCollaborators,
|
||||
},
|
||||
Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
|
||||
Connection, ConnectionId, ErrorCode, ErrorCodeExt, ErrorExt, Peer, Receipt, TypedEnvelope,
|
||||
};
|
||||
use serde::{Serialize, Serializer};
|
||||
use std::{
|
||||
@@ -543,12 +541,11 @@ impl Server {
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
peer.respond_with_error(
|
||||
receipt,
|
||||
proto::Error {
|
||||
message: error.to_string(),
|
||||
},
|
||||
)?;
|
||||
let proto_err = match &error {
|
||||
Error::Internal(err) => err.to_proto(),
|
||||
_ => ErrorCode::Internal.message(format!("{}", error)).to_proto(),
|
||||
};
|
||||
peer.respond_with_error(receipt, proto_err)?;
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
@@ -604,6 +601,7 @@ impl Server {
|
||||
let mut pool = this.connection_pool.lock();
|
||||
pool.add_connection(connection_id, user_id, user.admin);
|
||||
this.peer.send(connection_id, build_initial_contacts_update(contacts, &pool))?;
|
||||
this.peer.send(connection_id, build_update_user_channels(&channels_for_user.channel_memberships))?;
|
||||
this.peer.send(connection_id, build_channels_update(
|
||||
channels_for_user,
|
||||
channel_invites
|
||||
@@ -2302,10 +2300,7 @@ async fn create_channel(
|
||||
let db = session.db().await;
|
||||
|
||||
let parent_id = request.parent_id.map(|id| ChannelId::from_proto(id));
|
||||
let CreateChannelResult {
|
||||
channel,
|
||||
participants_to_update,
|
||||
} = db
|
||||
let (channel, owner, channel_members) = db
|
||||
.create_channel(&request.name, parent_id, session.user_id)
|
||||
.await?;
|
||||
|
||||
@@ -2315,12 +2310,29 @@ async fn create_channel(
|
||||
})?;
|
||||
|
||||
let connection_pool = session.connection_pool().await;
|
||||
for (user_id, channels) in participants_to_update {
|
||||
let update = build_channels_update(channels, vec![]);
|
||||
for connection_id in connection_pool.user_connection_ids(user_id) {
|
||||
if user_id == session.user_id {
|
||||
continue;
|
||||
}
|
||||
if let Some(owner) = owner {
|
||||
let update = proto::UpdateUserChannels {
|
||||
channel_memberships: vec![proto::ChannelMembership {
|
||||
channel_id: owner.channel_id.to_proto(),
|
||||
role: owner.role.into(),
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
for connection_id in connection_pool.user_connection_ids(owner.user_id) {
|
||||
session.peer.send(connection_id, update.clone())?;
|
||||
}
|
||||
}
|
||||
|
||||
for channel_member in channel_members {
|
||||
if !channel_member.role.can_see_channel(channel.visibility) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let update = proto::UpdateChannels {
|
||||
channels: vec![channel.to_proto()],
|
||||
..Default::default()
|
||||
};
|
||||
for connection_id in connection_pool.user_connection_ids(channel_member.user_id) {
|
||||
session.peer.send(connection_id, update.clone())?;
|
||||
}
|
||||
}
|
||||
@@ -2437,7 +2449,9 @@ async fn remove_channel_member(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Toggle the channel between public and private
|
||||
/// Toggle the channel between public and private.
|
||||
/// Care is taken to maintain the invariant that public channels only descend from public channels,
|
||||
/// (though members-only channels can appear at any point in the hierarchy).
|
||||
async fn set_channel_visibility(
|
||||
request: proto::SetChannelVisibility,
|
||||
response: Response<proto::SetChannelVisibility>,
|
||||
@@ -2447,27 +2461,25 @@ async fn set_channel_visibility(
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let visibility = request.visibility().into();
|
||||
|
||||
let SetChannelVisibilityResult {
|
||||
participants_to_update,
|
||||
participants_to_remove,
|
||||
channels_to_remove,
|
||||
} = db
|
||||
let (channel, channel_members) = db
|
||||
.set_channel_visibility(channel_id, visibility, session.user_id)
|
||||
.await?;
|
||||
|
||||
let connection_pool = session.connection_pool().await;
|
||||
for (user_id, channels) in participants_to_update {
|
||||
let update = build_channels_update(channels, vec![]);
|
||||
for connection_id in connection_pool.user_connection_ids(user_id) {
|
||||
session.peer.send(connection_id, update.clone())?;
|
||||
}
|
||||
}
|
||||
for user_id in participants_to_remove {
|
||||
let update = proto::UpdateChannels {
|
||||
delete_channels: channels_to_remove.iter().map(|id| id.to_proto()).collect(),
|
||||
..Default::default()
|
||||
for member in channel_members {
|
||||
let update = if member.role.can_see_channel(channel.visibility) {
|
||||
proto::UpdateChannels {
|
||||
channels: vec![channel.to_proto()],
|
||||
..Default::default()
|
||||
}
|
||||
} else {
|
||||
proto::UpdateChannels {
|
||||
delete_channels: vec![channel.id.to_proto()],
|
||||
..Default::default()
|
||||
}
|
||||
};
|
||||
for connection_id in connection_pool.user_connection_ids(user_id) {
|
||||
|
||||
for connection_id in connection_pool.user_connection_ids(member.user_id) {
|
||||
session.peer.send(connection_id, update.clone())?;
|
||||
}
|
||||
}
|
||||
@@ -2476,7 +2488,7 @@ async fn set_channel_visibility(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Alter the role for a user in the channel
|
||||
/// Alter the role for a user in the channel.
|
||||
async fn set_channel_member_role(
|
||||
request: proto::SetChannelMemberRole,
|
||||
response: Response<proto::SetChannelMemberRole>,
|
||||
@@ -2532,10 +2544,7 @@ async fn rename_channel(
|
||||
) -> Result<()> {
|
||||
let db = session.db().await;
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let RenameChannelResult {
|
||||
channel,
|
||||
participants_to_update,
|
||||
} = db
|
||||
let (channel, channel_members) = db
|
||||
.rename_channel(channel_id, session.user_id, &request.name)
|
||||
.await?;
|
||||
|
||||
@@ -2544,13 +2553,15 @@ async fn rename_channel(
|
||||
})?;
|
||||
|
||||
let connection_pool = session.connection_pool().await;
|
||||
for (user_id, channel) in participants_to_update {
|
||||
for connection_id in connection_pool.user_connection_ids(user_id) {
|
||||
let update = proto::UpdateChannels {
|
||||
channels: vec![channel.to_proto()],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
for channel_member in channel_members {
|
||||
if !channel_member.role.can_see_channel(channel.visibility) {
|
||||
continue;
|
||||
}
|
||||
let update = proto::UpdateChannels {
|
||||
channels: vec![channel.to_proto()],
|
||||
..Default::default()
|
||||
};
|
||||
for connection_id in connection_pool.user_connection_ids(channel_member.user_id) {
|
||||
session.peer.send(connection_id, update.clone())?;
|
||||
}
|
||||
}
|
||||
@@ -2565,49 +2576,41 @@ async fn move_channel(
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let to = request.to.map(ChannelId::from_proto);
|
||||
let to = ChannelId::from_proto(request.to);
|
||||
|
||||
let result = session
|
||||
let (channels, channel_members) = session
|
||||
.db()
|
||||
.await
|
||||
.move_channel(channel_id, to, session.user_id)
|
||||
.await?;
|
||||
|
||||
notify_channel_moved(result, session).await?;
|
||||
|
||||
response.send(Ack {})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn notify_channel_moved(result: Option<MoveChannelResult>, session: Session) -> Result<()> {
|
||||
let Some(MoveChannelResult {
|
||||
participants_to_remove,
|
||||
participants_to_update,
|
||||
moved_channels,
|
||||
}) = result
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let moved_channels: Vec<u64> = moved_channels.iter().map(|id| id.to_proto()).collect();
|
||||
|
||||
let connection_pool = session.connection_pool().await;
|
||||
for (user_id, channels) in participants_to_update {
|
||||
let mut update = build_channels_update(channels, vec![]);
|
||||
update.delete_channels = moved_channels.clone();
|
||||
for connection_id in connection_pool.user_connection_ids(user_id) {
|
||||
session.peer.send(connection_id, update.clone())?;
|
||||
for member in channel_members {
|
||||
let channels = channels
|
||||
.iter()
|
||||
.filter_map(|channel| {
|
||||
if member.role.can_see_channel(channel.visibility) {
|
||||
Some(channel.to_proto())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if channels.is_empty() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for user_id in participants_to_remove {
|
||||
let update = proto::UpdateChannels {
|
||||
delete_channels: moved_channels.clone(),
|
||||
channels,
|
||||
..Default::default()
|
||||
};
|
||||
for connection_id in connection_pool.user_connection_ids(user_id) {
|
||||
|
||||
for connection_id in connection_pool.user_connection_ids(member.user_id) {
|
||||
session.peer.send(connection_id, update.clone())?;
|
||||
}
|
||||
}
|
||||
|
||||
response.send(Ack {})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2837,7 +2840,7 @@ async fn update_channel_buffer(
|
||||
session.peer.send(
|
||||
peer_id.into(),
|
||||
proto::UpdateChannels {
|
||||
unseen_channel_buffer_changes: vec![proto::UnseenChannelBufferChange {
|
||||
latest_channel_buffer_versions: vec![proto::ChannelBufferVersion {
|
||||
channel_id: channel_id.to_proto(),
|
||||
epoch: epoch as u64,
|
||||
version: version.clone(),
|
||||
@@ -3023,7 +3026,7 @@ async fn send_channel_message(
|
||||
session.peer.send(
|
||||
peer_id.into(),
|
||||
proto::UpdateChannels {
|
||||
unseen_channel_messages: vec![proto::UnseenChannelMessage {
|
||||
latest_channel_message_ids: vec![proto::ChannelMessageId {
|
||||
channel_id: channel_id.to_proto(),
|
||||
message_id: message_id.to_proto(),
|
||||
}],
|
||||
@@ -3278,6 +3281,18 @@ fn notify_membership_updated(
|
||||
user_id: UserId,
|
||||
peer: &Peer,
|
||||
) {
|
||||
let user_channels_update = proto::UpdateUserChannels {
|
||||
channel_memberships: result
|
||||
.new_channels
|
||||
.channel_memberships
|
||||
.iter()
|
||||
.map(|cm| proto::ChannelMembership {
|
||||
channel_id: cm.channel_id.to_proto(),
|
||||
role: cm.role.into(),
|
||||
})
|
||||
.collect(),
|
||||
..Default::default()
|
||||
};
|
||||
let mut update = build_channels_update(result.new_channels, vec![]);
|
||||
update.delete_channels = result
|
||||
.removed_channels
|
||||
@@ -3287,10 +3302,27 @@ fn notify_membership_updated(
|
||||
update.remove_channel_invitations = vec![result.channel_id.to_proto()];
|
||||
|
||||
for connection_id in connection_pool.user_connection_ids(user_id) {
|
||||
peer.send(connection_id, user_channels_update.clone())
|
||||
.trace_err();
|
||||
peer.send(connection_id, update.clone()).trace_err();
|
||||
}
|
||||
}
|
||||
|
||||
fn build_update_user_channels(
|
||||
memberships: &Vec<db::channel_member::Model>,
|
||||
) -> proto::UpdateUserChannels {
|
||||
proto::UpdateUserChannels {
|
||||
channel_memberships: memberships
|
||||
.iter()
|
||||
.map(|m| proto::ChannelMembership {
|
||||
channel_id: m.channel_id.to_proto(),
|
||||
role: m.role.into(),
|
||||
})
|
||||
.collect(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn build_channels_update(
|
||||
channels: ChannelsForUser,
|
||||
channel_invites: Vec<db::Channel>,
|
||||
@@ -3301,8 +3333,8 @@ fn build_channels_update(
|
||||
update.channels.push(channel.to_proto());
|
||||
}
|
||||
|
||||
update.unseen_channel_buffer_changes = channels.unseen_buffer_changes;
|
||||
update.unseen_channel_messages = channels.channel_messages;
|
||||
update.latest_channel_buffer_versions = channels.latest_buffer_versions;
|
||||
update.latest_channel_message_ids = channels.latest_channel_messages;
|
||||
|
||||
for (channel_id, participants) in channels.channel_participants {
|
||||
update
|
||||
|
||||
@@ -637,7 +637,6 @@ async fn test_channel_buffer_changes(
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_channel_buffer_changed(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
assert!(has_buffer_changed);
|
||||
|
||||
@@ -655,7 +654,6 @@ async fn test_channel_buffer_changes(
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_channel_buffer_changed(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
assert!(!has_buffer_changed);
|
||||
|
||||
@@ -672,7 +670,6 @@ async fn test_channel_buffer_changes(
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_channel_buffer_changed(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
assert!(!has_buffer_changed);
|
||||
|
||||
@@ -687,7 +684,6 @@ async fn test_channel_buffer_changes(
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_channel_buffer_changed(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
assert!(!has_buffer_changed);
|
||||
|
||||
@@ -714,7 +710,6 @@ async fn test_channel_buffer_changes(
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_channel_buffer_changed(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
assert!(has_buffer_changed);
|
||||
}
|
||||
|
||||
@@ -195,6 +195,13 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |store, cx| {
|
||||
store.set_channel_visibility(parent_channel_id, proto::ChannelVisibility::Public, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |store, cx| {
|
||||
|
||||
@@ -313,7 +313,6 @@ async fn test_channel_message_changes(
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_new_messages(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
assert!(b_has_messages);
|
||||
@@ -341,7 +340,6 @@ async fn test_channel_message_changes(
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_new_messages(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
assert!(!b_has_messages);
|
||||
@@ -359,7 +357,6 @@ async fn test_channel_message_changes(
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_new_messages(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
assert!(!b_has_messages);
|
||||
@@ -382,7 +379,6 @@ async fn test_channel_message_changes(
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_new_messages(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
assert!(b_has_messages);
|
||||
@@ -402,7 +398,6 @@ async fn test_channel_message_changes(
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_new_messages(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
assert!(b_has_messages);
|
||||
|
||||
@@ -48,13 +48,11 @@ async fn test_core_channels(
|
||||
id: channel_a_id,
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_b_id,
|
||||
name: "channel-b".into(),
|
||||
depth: 1,
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
],
|
||||
);
|
||||
@@ -94,7 +92,6 @@ async fn test_core_channels(
|
||||
id: channel_a_id,
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
);
|
||||
|
||||
@@ -141,13 +138,11 @@ async fn test_core_channels(
|
||||
ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".into(),
|
||||
role: ChannelRole::Member,
|
||||
depth: 0,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_b_id,
|
||||
name: "channel-b".into(),
|
||||
role: ChannelRole::Member,
|
||||
depth: 1,
|
||||
},
|
||||
],
|
||||
@@ -169,19 +164,16 @@ async fn test_core_channels(
|
||||
ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".into(),
|
||||
role: ChannelRole::Member,
|
||||
depth: 0,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_b_id,
|
||||
name: "channel-b".into(),
|
||||
role: ChannelRole::Member,
|
||||
depth: 1,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_c_id,
|
||||
name: "channel-c".into(),
|
||||
role: ChannelRole::Member,
|
||||
depth: 2,
|
||||
},
|
||||
],
|
||||
@@ -213,19 +205,16 @@ async fn test_core_channels(
|
||||
id: channel_a_id,
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_b_id,
|
||||
name: "channel-b".into(),
|
||||
depth: 1,
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_c_id,
|
||||
name: "channel-c".into(),
|
||||
depth: 2,
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
],
|
||||
);
|
||||
@@ -247,7 +236,6 @@ async fn test_core_channels(
|
||||
id: channel_a_id,
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
}],
|
||||
);
|
||||
assert_channels(
|
||||
@@ -257,7 +245,6 @@ async fn test_core_channels(
|
||||
id: channel_a_id,
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
}],
|
||||
);
|
||||
|
||||
@@ -280,7 +267,6 @@ async fn test_core_channels(
|
||||
id: channel_a_id,
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
}],
|
||||
);
|
||||
|
||||
@@ -311,7 +297,6 @@ async fn test_core_channels(
|
||||
id: channel_a_id,
|
||||
name: "channel-a-renamed".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
}],
|
||||
);
|
||||
}
|
||||
@@ -420,7 +405,6 @@ async fn test_channel_room(
|
||||
id: zed_id,
|
||||
name: "zed".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
);
|
||||
cx_b.read(|cx| {
|
||||
@@ -681,7 +665,6 @@ async fn test_permissions_update_while_invited(
|
||||
depth: 0,
|
||||
id: rust_id,
|
||||
name: "rust".into(),
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
);
|
||||
assert_channels(client_b.channel_store(), cx_b, &[]);
|
||||
@@ -709,7 +692,6 @@ async fn test_permissions_update_while_invited(
|
||||
depth: 0,
|
||||
id: rust_id,
|
||||
name: "rust".into(),
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
);
|
||||
assert_channels(client_b.channel_store(), cx_b, &[]);
|
||||
@@ -748,7 +730,6 @@ async fn test_channel_rename(
|
||||
depth: 0,
|
||||
id: rust_id,
|
||||
name: "rust-archive".into(),
|
||||
role: ChannelRole::Admin,
|
||||
}],
|
||||
);
|
||||
|
||||
@@ -760,7 +741,6 @@ async fn test_channel_rename(
|
||||
depth: 0,
|
||||
id: rust_id,
|
||||
name: "rust-archive".into(),
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
);
|
||||
}
|
||||
@@ -889,7 +869,6 @@ async fn test_lost_channel_creation(
|
||||
depth: 0,
|
||||
id: channel_id,
|
||||
name: "x".into(),
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
);
|
||||
|
||||
@@ -913,13 +892,11 @@ async fn test_lost_channel_creation(
|
||||
depth: 0,
|
||||
id: channel_id,
|
||||
name: "x".into(),
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
ExpectedChannel {
|
||||
depth: 1,
|
||||
id: subchannel_id,
|
||||
name: "subchannel".into(),
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
],
|
||||
);
|
||||
@@ -944,13 +921,11 @@ async fn test_lost_channel_creation(
|
||||
depth: 0,
|
||||
id: channel_id,
|
||||
name: "x".into(),
|
||||
role: ChannelRole::Member,
|
||||
},
|
||||
ExpectedChannel {
|
||||
depth: 1,
|
||||
id: subchannel_id,
|
||||
name: "subchannel".into(),
|
||||
role: ChannelRole::Member,
|
||||
},
|
||||
],
|
||||
);
|
||||
@@ -1035,7 +1010,7 @@ async fn test_channel_link_notifications(
|
||||
let vim_channel = client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.create_channel("vim", None, cx)
|
||||
channel_store.create_channel("vim", Some(zed_channel), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1048,26 +1023,16 @@ async fn test_channel_link_notifications(
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.move_channel(vim_channel, Some(active_channel), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
|
||||
// the new channel shows for b and c
|
||||
assert_channels_list_shape(
|
||||
client_a.channel_store(),
|
||||
cx_a,
|
||||
&[(zed_channel, 0), (active_channel, 1), (vim_channel, 2)],
|
||||
&[(zed_channel, 0), (active_channel, 1), (vim_channel, 1)],
|
||||
);
|
||||
assert_channels_list_shape(
|
||||
client_b.channel_store(),
|
||||
cx_b,
|
||||
&[(zed_channel, 0), (active_channel, 1), (vim_channel, 2)],
|
||||
&[(zed_channel, 0), (active_channel, 1), (vim_channel, 1)],
|
||||
);
|
||||
assert_channels_list_shape(
|
||||
client_c.channel_store(),
|
||||
@@ -1078,7 +1043,7 @@ async fn test_channel_link_notifications(
|
||||
let helix_channel = client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.create_channel("helix", None, cx)
|
||||
channel_store.create_channel("helix", Some(zed_channel), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1086,7 +1051,7 @@ async fn test_channel_link_notifications(
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.move_channel(helix_channel, Some(vim_channel), cx)
|
||||
channel_store.move_channel(helix_channel, vim_channel, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1102,6 +1067,7 @@ async fn test_channel_link_notifications(
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.run_until_parked();
|
||||
|
||||
// the new channel shows for b and c
|
||||
assert_channels_list_shape(
|
||||
@@ -1110,8 +1076,8 @@ async fn test_channel_link_notifications(
|
||||
&[
|
||||
(zed_channel, 0),
|
||||
(active_channel, 1),
|
||||
(vim_channel, 2),
|
||||
(helix_channel, 3),
|
||||
(vim_channel, 1),
|
||||
(helix_channel, 2),
|
||||
],
|
||||
);
|
||||
assert_channels_list_shape(
|
||||
@@ -1119,41 +1085,6 @@ async fn test_channel_link_notifications(
|
||||
cx_c,
|
||||
&[(zed_channel, 0), (vim_channel, 1), (helix_channel, 2)],
|
||||
);
|
||||
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.set_channel_visibility(vim_channel, proto::ChannelVisibility::Members, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
|
||||
// the members-only channel is still shown for c, but hidden for b
|
||||
assert_channels_list_shape(
|
||||
client_b.channel_store(),
|
||||
cx_b,
|
||||
&[
|
||||
(zed_channel, 0),
|
||||
(active_channel, 1),
|
||||
(vim_channel, 2),
|
||||
(helix_channel, 3),
|
||||
],
|
||||
);
|
||||
cx_b.read(|cx| {
|
||||
client_b.channel_store().read_with(cx, |channel_store, _| {
|
||||
assert_eq!(
|
||||
channel_store
|
||||
.channel_for_id(vim_channel)
|
||||
.unwrap()
|
||||
.visibility,
|
||||
proto::ChannelVisibility::Members
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
assert_channels_list_shape(client_c.channel_store(), cx_c, &[(zed_channel, 0)]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -1170,24 +1101,20 @@ async fn test_channel_membership_notifications(
|
||||
|
||||
let channels = server
|
||||
.make_channel_tree(
|
||||
&[
|
||||
("zed", None),
|
||||
("active", Some("zed")),
|
||||
("vim", Some("active")),
|
||||
],
|
||||
&[("zed", None), ("vim", Some("zed")), ("opensource", None)],
|
||||
(&client_a, cx_a),
|
||||
)
|
||||
.await;
|
||||
let zed_channel = channels[0];
|
||||
let _active_channel = channels[1];
|
||||
let vim_channel = channels[2];
|
||||
let vim_channel = channels[1];
|
||||
let opensource_channel = channels[2];
|
||||
|
||||
try_join_all(client_a.channel_store().update(cx_a, |channel_store, cx| {
|
||||
[
|
||||
channel_store.set_channel_visibility(zed_channel, proto::ChannelVisibility::Public, cx),
|
||||
channel_store.set_channel_visibility(vim_channel, proto::ChannelVisibility::Public, cx),
|
||||
channel_store.invite_member(vim_channel, user_b, proto::ChannelRole::Member, cx),
|
||||
channel_store.invite_member(zed_channel, user_b, proto::ChannelRole::Guest, cx),
|
||||
channel_store.invite_member(zed_channel, user_b, proto::ChannelRole::Admin, cx),
|
||||
channel_store.invite_member(opensource_channel, user_b, proto::ChannelRole::Member, cx),
|
||||
]
|
||||
}))
|
||||
.await
|
||||
@@ -1203,14 +1130,6 @@ async fn test_channel_membership_notifications(
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
client_b
|
||||
.channel_store()
|
||||
.update(cx_b, |channel_store, cx| {
|
||||
channel_store.respond_to_channel_invite(vim_channel, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
|
||||
// we have an admin (a), and a guest (b) with access to all of zed, and membership in vim.
|
||||
@@ -1222,45 +1141,42 @@ async fn test_channel_membership_notifications(
|
||||
depth: 0,
|
||||
id: zed_channel,
|
||||
name: "zed".into(),
|
||||
role: ChannelRole::Guest,
|
||||
},
|
||||
ExpectedChannel {
|
||||
depth: 1,
|
||||
id: vim_channel,
|
||||
name: "vim".into(),
|
||||
role: ChannelRole::Member,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
client_a
|
||||
client_b.channel_store().update(cx_b, |channel_store, _| {
|
||||
channel_store.is_channel_admin(zed_channel)
|
||||
});
|
||||
|
||||
client_b
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.remove_member(vim_channel, user_b, cx)
|
||||
.update(cx_b, |channel_store, cx| {
|
||||
channel_store.respond_to_channel_invite(opensource_channel, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
cx_a.run_until_parked();
|
||||
|
||||
assert_channels(
|
||||
client_b.channel_store(),
|
||||
cx_b,
|
||||
&[
|
||||
ExpectedChannel {
|
||||
depth: 0,
|
||||
id: zed_channel,
|
||||
name: "zed".into(),
|
||||
role: ChannelRole::Guest,
|
||||
},
|
||||
ExpectedChannel {
|
||||
depth: 1,
|
||||
id: vim_channel,
|
||||
name: "vim".into(),
|
||||
role: ChannelRole::Guest,
|
||||
},
|
||||
],
|
||||
)
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.set_member_role(opensource_channel, user_b, ChannelRole::Admin, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx_a.run_until_parked();
|
||||
|
||||
client_b.channel_store().update(cx_b, |channel_store, _| {
|
||||
channel_store.is_channel_admin(opensource_channel)
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -1329,25 +1245,6 @@ async fn test_guest_access(
|
||||
assert_eq!(participants.len(), 1);
|
||||
assert_eq!(participants[0].id, client_b.user_id().unwrap());
|
||||
});
|
||||
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.set_channel_visibility(channel_a, proto::ChannelVisibility::Members, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
executor.run_until_parked();
|
||||
|
||||
assert_channels_list_shape(client_b.channel_store(), cx_b, &[]);
|
||||
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_b, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
assert_channels_list_shape(client_b.channel_store(), cx_b, &[(channel_b, 0)]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -1451,7 +1348,7 @@ async fn test_channel_moving(
|
||||
client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
channel_store.move_channel(channel_d_id, Some(channel_b_id), cx)
|
||||
channel_store.move_channel(channel_d_id, channel_b_id, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1476,7 +1373,6 @@ struct ExpectedChannel {
|
||||
depth: usize,
|
||||
id: ChannelId,
|
||||
name: SharedString,
|
||||
role: ChannelRole,
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
@@ -1494,7 +1390,6 @@ fn assert_channel_invitations(
|
||||
depth: 0,
|
||||
name: channel.name.clone(),
|
||||
id: channel.id,
|
||||
role: channel.role,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
@@ -1516,7 +1411,6 @@ fn assert_channels(
|
||||
depth,
|
||||
name: channel.name.clone().into(),
|
||||
id: channel.id,
|
||||
role: channel.role,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
|
||||
@@ -3884,6 +3884,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
|
||||
// Join project as client C and observe the diagnostics.
|
||||
let project_c = client_c.build_remote_project(project_id, cx_c).await;
|
||||
executor.run_until_parked();
|
||||
let project_c_diagnostic_summaries =
|
||||
Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>()
|
||||
|
||||
@@ -125,6 +125,7 @@ impl TestServer {
|
||||
let channel_id = server
|
||||
.make_channel("a", None, (&client_a, cx_a), &mut [(&client_b, cx_b)])
|
||||
.await;
|
||||
cx_a.run_until_parked();
|
||||
|
||||
(client_a, client_b, channel_id)
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ impl ChannelView {
|
||||
} else {
|
||||
self.channel_store.update(cx, |store, cx| {
|
||||
let channel_buffer = self.channel_buffer.read(cx);
|
||||
store.notes_changed(
|
||||
store.update_latest_notes_version(
|
||||
channel_buffer.channel_id,
|
||||
channel_buffer.epoch(),
|
||||
&channel_buffer.buffer().read(cx).version(),
|
||||
|
||||
@@ -274,7 +274,7 @@ impl ChatPanel {
|
||||
} => {
|
||||
if !self.active {
|
||||
self.channel_store.update(cx, |store, cx| {
|
||||
store.new_message(*channel_id, *message_id, cx)
|
||||
store.update_latest_message_id(*channel_id, *message_id, cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,10 @@ use gpui::{
|
||||
};
|
||||
use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrev};
|
||||
use project::{Fs, Project};
|
||||
use rpc::proto::{self, PeerId};
|
||||
use rpc::{
|
||||
proto::{self, ChannelVisibility, PeerId},
|
||||
ErrorCode, ErrorExt,
|
||||
};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use smallvec::SmallVec;
|
||||
@@ -35,7 +38,7 @@ use ui::{
|
||||
use util::{maybe, ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
notifications::{NotifyResultExt, NotifyTaskExt},
|
||||
notifications::{DetachAndPromptErr, NotifyResultExt, NotifyTaskExt},
|
||||
Workspace,
|
||||
};
|
||||
|
||||
@@ -879,7 +882,7 @@ impl CollabPanel {
|
||||
.update(cx, |workspace, cx| {
|
||||
let app_state = workspace.app_state().clone();
|
||||
workspace::join_remote_project(project_id, host_user_id, app_state, cx)
|
||||
.detach_and_log_err(cx);
|
||||
.detach_and_prompt_err("Failed to join project", cx, |_, _| None);
|
||||
})
|
||||
.ok();
|
||||
}))
|
||||
@@ -1017,7 +1020,12 @@ impl CollabPanel {
|
||||
)
|
||||
})
|
||||
})
|
||||
.detach_and_notify_err(cx)
|
||||
.detach_and_prompt_err("Failed to grant write access", cx, |e, _| {
|
||||
match e.error_code() {
|
||||
ErrorCode::NeedsCla => Some("This user has not yet signed the CLA at https://zed.dev/cla.".into()),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
} else if role == proto::ChannelRole::Member {
|
||||
@@ -1038,7 +1046,7 @@ impl CollabPanel {
|
||||
)
|
||||
})
|
||||
})
|
||||
.detach_and_notify_err(cx)
|
||||
.detach_and_prompt_err("Failed to revoke write access", cx, |_, _| None)
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
@@ -1126,13 +1134,6 @@ impl CollabPanel {
|
||||
"Rename",
|
||||
Some(Box::new(SecondaryConfirm)),
|
||||
cx.handler_for(&this, move |this, cx| this.rename_channel(channel_id, cx)),
|
||||
)
|
||||
.entry(
|
||||
"Move this channel",
|
||||
None,
|
||||
cx.handler_for(&this, move |this, cx| {
|
||||
this.start_move_channel(channel_id, cx)
|
||||
}),
|
||||
);
|
||||
|
||||
if let Some(channel_name) = clipboard_channel_name {
|
||||
@@ -1145,23 +1146,52 @@ impl CollabPanel {
|
||||
);
|
||||
}
|
||||
|
||||
context_menu = context_menu
|
||||
.separator()
|
||||
.entry(
|
||||
"Invite Members",
|
||||
None,
|
||||
cx.handler_for(&this, move |this, cx| this.invite_members(channel_id, cx)),
|
||||
)
|
||||
.entry(
|
||||
if self.channel_store.read(cx).is_root_channel(channel_id) {
|
||||
context_menu = context_menu.separator().entry(
|
||||
"Manage Members",
|
||||
None,
|
||||
cx.handler_for(&this, move |this, cx| this.manage_members(channel_id, cx)),
|
||||
)
|
||||
.entry(
|
||||
"Delete",
|
||||
} else {
|
||||
context_menu = context_menu.entry(
|
||||
"Move this channel",
|
||||
None,
|
||||
cx.handler_for(&this, move |this, cx| this.remove_channel(channel_id, cx)),
|
||||
cx.handler_for(&this, move |this, cx| {
|
||||
this.start_move_channel(channel_id, cx)
|
||||
}),
|
||||
);
|
||||
if self.channel_store.read(cx).is_public_channel(channel_id) {
|
||||
context_menu = context_menu.separator().entry(
|
||||
"Make Channel Private",
|
||||
None,
|
||||
cx.handler_for(&this, move |this, cx| {
|
||||
this.set_channel_visibility(
|
||||
channel_id,
|
||||
ChannelVisibility::Members,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
context_menu = context_menu.separator().entry(
|
||||
"Make Channel Public",
|
||||
None,
|
||||
cx.handler_for(&this, move |this, cx| {
|
||||
this.set_channel_visibility(
|
||||
channel_id,
|
||||
ChannelVisibility::Public,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
context_menu = context_menu.entry(
|
||||
"Delete",
|
||||
None,
|
||||
cx.handler_for(&this, move |this, cx| this.remove_channel(channel_id, cx)),
|
||||
);
|
||||
}
|
||||
|
||||
context_menu
|
||||
@@ -1258,7 +1288,11 @@ impl CollabPanel {
|
||||
app_state,
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
.detach_and_prompt_err(
|
||||
"Failed to join project",
|
||||
cx,
|
||||
|_, _| None,
|
||||
);
|
||||
}
|
||||
}
|
||||
ListEntry::ParticipantScreen { peer_id, .. } => {
|
||||
@@ -1432,7 +1466,7 @@ impl CollabPanel {
|
||||
fn leave_call(cx: &mut WindowContext) {
|
||||
ActiveCall::global(cx)
|
||||
.update(cx, |call, cx| call.hang_up(cx))
|
||||
.detach_and_log_err(cx);
|
||||
.detach_and_prompt_err("Failed to hang up", cx, |_, _| None);
|
||||
}
|
||||
|
||||
fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
|
||||
@@ -1478,10 +1512,6 @@ impl CollabPanel {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn invite_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
|
||||
self.show_channel_modal(channel_id, channel_modal::Mode::InviteMembers, cx);
|
||||
}
|
||||
|
||||
fn manage_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
|
||||
self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx);
|
||||
}
|
||||
@@ -1518,6 +1548,27 @@ impl CollabPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_channel_visibility(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
visibility: ChannelVisibility,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.channel_store
|
||||
.update(cx, |channel_store, cx| {
|
||||
channel_store.set_channel_visibility(channel_id, visibility, cx)
|
||||
})
|
||||
.detach_and_prompt_err("Failed to set channel visibility", cx, |e, _| match e.error_code() {
|
||||
ErrorCode::BadPublicNesting =>
|
||||
if e.error_tag("direction") == Some("parent") {
|
||||
Some("To make a channel public, its parent channel must be public.".to_string())
|
||||
} else {
|
||||
Some("To make a channel private, all of its subchannels must be private.".to_string())
|
||||
},
|
||||
_ => None
|
||||
});
|
||||
}
|
||||
|
||||
fn start_move_channel(&mut self, channel_id: ChannelId, _cx: &mut ViewContext<Self>) {
|
||||
self.channel_clipboard = Some(ChannelMoveClipboard { channel_id });
|
||||
}
|
||||
@@ -1534,14 +1585,27 @@ impl CollabPanel {
|
||||
cx: &mut ViewContext<CollabPanel>,
|
||||
) {
|
||||
if let Some(clipboard) = self.channel_clipboard.take() {
|
||||
self.channel_store.update(cx, |channel_store, cx| {
|
||||
channel_store
|
||||
.move_channel(clipboard.channel_id, Some(to_channel_id), cx)
|
||||
.detach_and_log_err(cx)
|
||||
})
|
||||
self.move_channel(clipboard.channel_id, to_channel_id, cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn move_channel(&self, channel_id: ChannelId, to: ChannelId, cx: &mut ViewContext<Self>) {
|
||||
self.channel_store
|
||||
.update(cx, |channel_store, cx| {
|
||||
channel_store.move_channel(channel_id, to, cx)
|
||||
})
|
||||
.detach_and_prompt_err("Failed to move channel", cx, |e, _| match e.error_code() {
|
||||
ErrorCode::BadPublicNesting => {
|
||||
Some("Public channels must have public parents".into())
|
||||
}
|
||||
ErrorCode::CircularNesting => Some("You cannot move a channel into itself".into()),
|
||||
ErrorCode::WrongMoveTarget => {
|
||||
Some("You cannot move a channel into a different root channel".into())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn open_channel_notes(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
ChannelView::open(channel_id, workspace, cx).detach();
|
||||
@@ -1610,7 +1674,12 @@ impl CollabPanel {
|
||||
"Are you sure you want to remove the channel \"{}\"?",
|
||||
channel.name
|
||||
);
|
||||
let answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
|
||||
let answer = cx.prompt(
|
||||
PromptLevel::Warning,
|
||||
&prompt_message,
|
||||
None,
|
||||
&["Remove", "Cancel"],
|
||||
);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if answer.await? == 0 {
|
||||
channel_store
|
||||
@@ -1631,7 +1700,12 @@ impl CollabPanel {
|
||||
"Are you sure you want to remove \"{}\" from your contacts?",
|
||||
github_login
|
||||
);
|
||||
let answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
|
||||
let answer = cx.prompt(
|
||||
PromptLevel::Warning,
|
||||
&prompt_message,
|
||||
None,
|
||||
&["Remove", "Cancel"],
|
||||
);
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
if answer.await? == 0 {
|
||||
user_store
|
||||
@@ -1641,7 +1715,7 @@ impl CollabPanel {
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
.detach_and_prompt_err("Failed to remove contact", cx, |_, _| None);
|
||||
}
|
||||
|
||||
fn respond_to_contact_request(
|
||||
@@ -1654,7 +1728,7 @@ impl CollabPanel {
|
||||
.update(cx, |store, cx| {
|
||||
store.respond_to_contact_request(user_id, accept, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
.detach_and_prompt_err("Failed to respond to contact request", cx, |_, _| None);
|
||||
}
|
||||
|
||||
fn respond_to_channel_invite(
|
||||
@@ -1675,7 +1749,7 @@ impl CollabPanel {
|
||||
.update(cx, |call, cx| {
|
||||
call.invite(recipient_user_id, Some(self.project.clone()), cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
.detach_and_prompt_err("Call failed", cx, |_, _| None);
|
||||
}
|
||||
|
||||
fn join_channel(&self, channel_id: u64, cx: &mut ViewContext<Self>) {
|
||||
@@ -1691,7 +1765,7 @@ impl CollabPanel {
|
||||
Some(handle),
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx)
|
||||
.detach_and_prompt_err("Failed to join channel", cx, |_, _| None)
|
||||
}
|
||||
|
||||
fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
|
||||
@@ -1704,7 +1778,7 @@ impl CollabPanel {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel
|
||||
.select_channel(channel_id, None, cx)
|
||||
.detach_and_log_err(cx);
|
||||
.detach_and_notify_err(cx);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1958,32 +2032,19 @@ impl CollabPanel {
|
||||
| Section::Offline => true,
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
.group("section-header")
|
||||
.child(
|
||||
ListHeader::new(text)
|
||||
.when(can_collapse, |header| {
|
||||
header.toggle(Some(!is_collapsed)).on_toggle(cx.listener(
|
||||
move |this, _, cx| {
|
||||
this.toggle_section_expanded(section, cx);
|
||||
},
|
||||
))
|
||||
})
|
||||
.inset(true)
|
||||
.end_slot::<AnyElement>(button)
|
||||
.selected(is_selected),
|
||||
)
|
||||
.when(section == Section::Channels, |el| {
|
||||
el.drag_over::<Channel>(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
||||
.on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| {
|
||||
this.channel_store
|
||||
.update(cx, |channel_store, cx| {
|
||||
channel_store.move_channel(dragged_channel.id, None, cx)
|
||||
})
|
||||
.detach_and_log_err(cx)
|
||||
}))
|
||||
})
|
||||
h_flex().w_full().group("section-header").child(
|
||||
ListHeader::new(text)
|
||||
.when(can_collapse, |header| {
|
||||
header
|
||||
.toggle(Some(!is_collapsed))
|
||||
.on_toggle(cx.listener(move |this, _, cx| {
|
||||
this.toggle_section_expanded(section, cx);
|
||||
}))
|
||||
})
|
||||
.inset(true)
|
||||
.end_slot::<AnyElement>(button)
|
||||
.selected(is_selected),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_contact(
|
||||
@@ -2197,17 +2258,16 @@ impl CollabPanel {
|
||||
Some(call_channel == channel_id)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
let is_public = self
|
||||
.channel_store
|
||||
.read(cx)
|
||||
let channel_store = self.channel_store.read(cx);
|
||||
let is_public = channel_store
|
||||
.channel_for_id(channel_id)
|
||||
.map(|channel| channel.visibility)
|
||||
== Some(proto::ChannelVisibility::Public);
|
||||
let disclosed =
|
||||
has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok());
|
||||
|
||||
let has_messages_notification = channel.unseen_message_id.is_some();
|
||||
let has_notes_notification = channel.unseen_note_version.is_some();
|
||||
let has_messages_notification = channel_store.has_new_messages(channel_id);
|
||||
let has_notes_notification = channel_store.has_channel_buffer_changed(channel_id);
|
||||
|
||||
const FACEPILE_LIMIT: usize = 3;
|
||||
let participants = self.channel_store.read(cx).channel_participants(channel_id);
|
||||
@@ -2238,25 +2298,35 @@ impl CollabPanel {
|
||||
};
|
||||
|
||||
let width = self.width.unwrap_or(px(240.));
|
||||
let root_id = channel.root_id();
|
||||
|
||||
div()
|
||||
.id(channel_id as usize)
|
||||
.group("")
|
||||
.flex()
|
||||
.w_full()
|
||||
.on_drag(channel.clone(), move |channel, cx| {
|
||||
cx.new_view(|_| DraggedChannelView {
|
||||
channel: channel.clone(),
|
||||
width,
|
||||
.when(!channel.is_root_channel(), |el| {
|
||||
el.on_drag(channel.clone(), move |channel, cx| {
|
||||
cx.new_view(|_| DraggedChannelView {
|
||||
channel: channel.clone(),
|
||||
width,
|
||||
})
|
||||
})
|
||||
})
|
||||
.drag_over::<Channel>(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
||||
.drag_over::<Channel>({
|
||||
move |style, dragged_channel: &Channel, cx| {
|
||||
if dragged_channel.root_id() == root_id {
|
||||
style.bg(cx.theme().colors().ghost_element_hover)
|
||||
} else {
|
||||
style
|
||||
}
|
||||
}
|
||||
})
|
||||
.on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| {
|
||||
this.channel_store
|
||||
.update(cx, |channel_store, cx| {
|
||||
channel_store.move_channel(dragged_channel.id, Some(channel_id), cx)
|
||||
})
|
||||
.detach_and_log_err(cx)
|
||||
if dragged_channel.root_id() != root_id {
|
||||
return;
|
||||
}
|
||||
this.move_channel(dragged_channel.id, channel_id, cx);
|
||||
}))
|
||||
.child(
|
||||
ListItem::new(channel_id as usize)
|
||||
@@ -2302,9 +2372,8 @@ impl CollabPanel {
|
||||
h_flex()
|
||||
.absolute()
|
||||
.right(rems(0.))
|
||||
.z_index(1)
|
||||
.h_full()
|
||||
// HACK: Without this the channel name clips on top of the icons, but I'm not sure why.
|
||||
.z_index(10)
|
||||
.child(
|
||||
h_flex()
|
||||
.h_full()
|
||||
|
||||
@@ -10,11 +10,10 @@ use gpui::{
|
||||
WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use rpc::proto::channel_member;
|
||||
use std::sync::Arc;
|
||||
use ui::{prelude::*, Avatar, Checkbox, ContextMenu, ListItem, ListItemSpacing};
|
||||
use util::TryFutureExt;
|
||||
use workspace::{notifications::NotifyTaskExt, ModalView};
|
||||
use workspace::{notifications::DetachAndPromptErr, ModalView};
|
||||
|
||||
actions!(
|
||||
channel_modal,
|
||||
@@ -359,10 +358,8 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
Some(proto::channel_member::Kind::Invitee) => {
|
||||
self.remove_member(selected_user.id, cx);
|
||||
}
|
||||
Some(proto::channel_member::Kind::AncestorMember) | None => {
|
||||
self.invite_member(selected_user, cx)
|
||||
}
|
||||
Some(proto::channel_member::Kind::Member) => {}
|
||||
None => self.invite_member(selected_user, cx),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -402,10 +399,6 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
.children(
|
||||
if request_status == Some(proto::channel_member::Kind::Invitee) {
|
||||
Some(Label::new("Invited"))
|
||||
} else if membership.map(|m| m.kind)
|
||||
== Some(channel_member::Kind::AncestorMember)
|
||||
{
|
||||
Some(Label::new("Parent"))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
@@ -498,7 +491,7 @@ impl ChannelModalDelegate {
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detach_and_notify_err(cx);
|
||||
.detach_and_prompt_err("Failed to update role", cx, |_, _| None);
|
||||
Some(())
|
||||
}
|
||||
|
||||
@@ -530,7 +523,7 @@ impl ChannelModalDelegate {
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detach_and_notify_err(cx);
|
||||
.detach_and_prompt_err("Failed to remove member", cx, |_, _| None);
|
||||
Some(())
|
||||
}
|
||||
|
||||
@@ -556,23 +549,16 @@ impl ChannelModalDelegate {
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detach_and_notify_err(cx);
|
||||
.detach_and_prompt_err("Failed to invite member", cx, |_, _| None);
|
||||
}
|
||||
|
||||
fn show_context_menu(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
|
||||
let Some(membership) = self.member_at_index(ix) else {
|
||||
return;
|
||||
};
|
||||
if membership.kind == proto::channel_member::Kind::AncestorMember {
|
||||
return;
|
||||
}
|
||||
let user_id = membership.user.id;
|
||||
let picker = cx.view().clone();
|
||||
let context_menu = ContextMenu::build(cx, |mut menu, _cx| {
|
||||
if membership.kind == channel_member::Kind::AncestorMember {
|
||||
return menu.entry("Inherited membership", None, |_| {});
|
||||
};
|
||||
|
||||
let role = membership.role;
|
||||
|
||||
if role == ChannelRole::Admin || role == ChannelRole::Member {
|
||||
|
||||
@@ -567,7 +567,7 @@ impl EditorElement {
|
||||
cx,
|
||||
);
|
||||
hover_at(editor, Some(point), cx);
|
||||
Self::update_visible_cursor(editor, point, cx);
|
||||
Self::update_visible_cursor(editor, point, position_map, cx);
|
||||
}
|
||||
None => {
|
||||
update_inlay_link_and_hover_points(
|
||||
@@ -592,9 +592,10 @@ impl EditorElement {
|
||||
fn update_visible_cursor(
|
||||
editor: &mut Editor,
|
||||
point: DisplayPoint,
|
||||
position_map: &PositionMap,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let snapshot = &position_map.snapshot;
|
||||
let Some(hub) = editor.collaboration_hub() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -31,7 +31,8 @@ pub fn init(cx: &mut AppContext) {
|
||||
|
||||
let prompt = cx.prompt(
|
||||
PromptLevel::Info,
|
||||
&format!("Copied into clipboard:\n\n{specs}"),
|
||||
"Copied into clipboard",
|
||||
Some(&specs),
|
||||
&["OK"],
|
||||
);
|
||||
cx.spawn(|_, _cx| async move {
|
||||
|
||||
@@ -97,7 +97,7 @@ impl ModalView for FeedbackModal {
|
||||
return true;
|
||||
}
|
||||
|
||||
let answer = cx.prompt(PromptLevel::Info, "Discard feedback?", &["Yes", "No"]);
|
||||
let answer = cx.prompt(PromptLevel::Info, "Discard feedback?", None, &["Yes", "No"]);
|
||||
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
if answer.await.ok() == Some(0) {
|
||||
@@ -222,6 +222,7 @@ impl FeedbackModal {
|
||||
let answer = cx.prompt(
|
||||
PromptLevel::Info,
|
||||
"Ready to submit your feedback?",
|
||||
None,
|
||||
&["Yes, Submit!", "No"],
|
||||
);
|
||||
let client = cx.global::<Arc<Client>>().clone();
|
||||
@@ -255,6 +256,7 @@ impl FeedbackModal {
|
||||
let prompt = cx.prompt(
|
||||
PromptLevel::Critical,
|
||||
FEEDBACK_SUBMISSION_ERROR_TEXT,
|
||||
None,
|
||||
&["OK"],
|
||||
);
|
||||
cx.spawn(|_, _cx| async move {
|
||||
|
||||
@@ -3,7 +3,7 @@ use gpui::AppContext;
|
||||
use human_bytes::human_bytes;
|
||||
use serde::Serialize;
|
||||
use std::{env, fmt::Display};
|
||||
use sysinfo::{System, SystemExt};
|
||||
use sysinfo::{RefreshKind, System, SystemExt};
|
||||
use util::channel::ReleaseChannel;
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
@@ -23,7 +23,7 @@ impl SystemSpecs {
|
||||
.map(|v| v.to_string());
|
||||
let release_channel = cx.global::<ReleaseChannel>().display_name();
|
||||
let os_name = cx.app_metadata().os_name;
|
||||
let system = System::new_all();
|
||||
let system = System::new_with_specifics(RefreshKind::new().with_memory());
|
||||
let memory = system.total_memory();
|
||||
let architecture = env::consts::ARCH;
|
||||
let os_version = cx
|
||||
|
||||
@@ -45,7 +45,7 @@ impl<'a> Matcher<'a> {
|
||||
lowercase_query,
|
||||
query_char_bag,
|
||||
min_score: 0.0,
|
||||
last_positions: vec![0; query.len()],
|
||||
last_positions: vec![0; lowercase_query.len()],
|
||||
match_positions: vec![0; query.len()],
|
||||
score_matrix: Vec::new(),
|
||||
best_position_matrix: Vec::new(),
|
||||
@@ -82,7 +82,7 @@ impl<'a> Matcher<'a> {
|
||||
lowercase_candidate_chars.clear();
|
||||
for c in candidate.to_string().chars() {
|
||||
candidate_chars.push(c);
|
||||
lowercase_candidate_chars.push(c.to_ascii_lowercase());
|
||||
lowercase_candidate_chars.append(&mut c.to_lowercase().collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
if !self.find_last_positions(lowercase_prefix, &lowercase_candidate_chars) {
|
||||
@@ -383,6 +383,25 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lowercase_longer_than_uppercase() {
|
||||
// This character has more chars in lower-case than in upper-case.
|
||||
let paths = vec!["\u{0130}"];
|
||||
let query = "\u{0130}";
|
||||
assert_eq!(
|
||||
match_single_path_query(query, false, &paths),
|
||||
vec![("\u{0130}", vec![0])]
|
||||
);
|
||||
|
||||
// Path is the lower-case version of the query
|
||||
let paths = vec!["i\u{307}"];
|
||||
let query = "\u{0130}";
|
||||
assert_eq!(
|
||||
match_single_path_query(query, false, &paths),
|
||||
vec![("i\u{307}", vec![0])]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_match_multibyte_path_entries() {
|
||||
let paths = vec!["aαbβ/cγdδ", "αβγδ/bcde", "c1️⃣2️⃣3️⃣/d4️⃣5️⃣6️⃣/e7️⃣8️⃣9️⃣/f", "/d/🆒/h"];
|
||||
|
||||
@@ -136,25 +136,6 @@ impl Render for () {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {}
|
||||
}
|
||||
|
||||
/// A quick way to create a [`Render`]able view without having to define a new type.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct TestView(Box<dyn FnMut(&mut ViewContext<TestView>) -> AnyElement>);
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl TestView {
|
||||
/// Construct a TestView from a render closure.
|
||||
pub fn new<F: FnMut(&mut ViewContext<TestView>) -> AnyElement + 'static>(f: F) -> Self {
|
||||
Self(Box::new(f))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl Render for TestView {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
(self.0)(cx)
|
||||
}
|
||||
}
|
||||
|
||||
/// You can derive [`IntoElement`] on any type that implements this trait.
|
||||
/// It is used to construct reusable `components` out of plain data. Think of
|
||||
/// components as a recipe for a certain pattern of elements. RenderOnce allows
|
||||
|
||||
@@ -782,10 +782,20 @@ pub trait InteractiveElement: Sized {
|
||||
}
|
||||
|
||||
/// Apply the given style when the given data type is dragged over this element
|
||||
fn drag_over<S: 'static>(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self {
|
||||
self.interactivity()
|
||||
.drag_over_styles
|
||||
.push((TypeId::of::<S>(), f(StyleRefinement::default())));
|
||||
fn drag_over<S: 'static>(
|
||||
mut self,
|
||||
f: impl 'static + Fn(StyleRefinement, &S, &WindowContext) -> StyleRefinement,
|
||||
) -> Self {
|
||||
self.interactivity().drag_over_styles.push((
|
||||
TypeId::of::<S>(),
|
||||
Box::new(move |currently_dragged: &dyn Any, cx| {
|
||||
f(
|
||||
StyleRefinement::default(),
|
||||
currently_dragged.downcast_ref::<S>().unwrap(),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
));
|
||||
self
|
||||
}
|
||||
|
||||
@@ -1174,7 +1184,10 @@ pub struct Interactivity {
|
||||
pub(crate) group_hover_style: Option<GroupStyle>,
|
||||
pub(crate) active_style: Option<Box<StyleRefinement>>,
|
||||
pub(crate) group_active_style: Option<GroupStyle>,
|
||||
pub(crate) drag_over_styles: Vec<(TypeId, StyleRefinement)>,
|
||||
pub(crate) drag_over_styles: Vec<(
|
||||
TypeId,
|
||||
Box<dyn Fn(&dyn Any, &mut WindowContext) -> StyleRefinement>,
|
||||
)>,
|
||||
pub(crate) group_drag_over_styles: Vec<(TypeId, GroupStyle)>,
|
||||
pub(crate) mouse_down_listeners: Vec<MouseDownListener>,
|
||||
pub(crate) mouse_up_listeners: Vec<MouseUpListener>,
|
||||
@@ -1980,7 +1993,7 @@ impl Interactivity {
|
||||
}
|
||||
}
|
||||
|
||||
for (state_type, drag_over_style) in &self.drag_over_styles {
|
||||
for (state_type, build_drag_over_style) in &self.drag_over_styles {
|
||||
if *state_type == drag.value.as_ref().type_id()
|
||||
&& bounds
|
||||
.intersect(&cx.content_mask().bounds)
|
||||
@@ -1990,7 +2003,7 @@ impl Interactivity {
|
||||
cx.stacking_order(),
|
||||
)
|
||||
{
|
||||
style.refine(drag_over_style);
|
||||
style.refine(&build_drag_over_style(drag.value.as_ref(), cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +150,13 @@ pub(crate) trait PlatformWindow {
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||
fn set_input_handler(&mut self, input_handler: PlatformInputHandler);
|
||||
fn take_input_handler(&mut self) -> Option<PlatformInputHandler>;
|
||||
fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize>;
|
||||
fn prompt(
|
||||
&self,
|
||||
level: PromptLevel,
|
||||
msg: &str,
|
||||
detail: Option<&str>,
|
||||
answers: &[&str],
|
||||
) -> oneshot::Receiver<usize>;
|
||||
fn activate(&self);
|
||||
fn set_title(&mut self, title: &str);
|
||||
fn set_edited(&mut self, edited: bool);
|
||||
|
||||
@@ -534,67 +534,77 @@ impl Platform for MacPlatform {
|
||||
&self,
|
||||
options: PathPromptOptions,
|
||||
) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
|
||||
unsafe {
|
||||
let panel = NSOpenPanel::openPanel(nil);
|
||||
panel.setCanChooseDirectories_(options.directories.to_objc());
|
||||
panel.setCanChooseFiles_(options.files.to_objc());
|
||||
panel.setAllowsMultipleSelection_(options.multiple.to_objc());
|
||||
panel.setResolvesAliases_(false.to_objc());
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
let done_tx = Cell::new(Some(done_tx));
|
||||
let block = ConcreteBlock::new(move |response: NSModalResponse| {
|
||||
let result = if response == NSModalResponse::NSModalResponseOk {
|
||||
let mut result = Vec::new();
|
||||
let urls = panel.URLs();
|
||||
for i in 0..urls.count() {
|
||||
let url = urls.objectAtIndex(i);
|
||||
if url.isFileURL() == YES {
|
||||
if let Ok(path) = ns_url_to_path(url) {
|
||||
result.push(path)
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
self.foreground_executor()
|
||||
.spawn(async move {
|
||||
unsafe {
|
||||
let panel = NSOpenPanel::openPanel(nil);
|
||||
panel.setCanChooseDirectories_(options.directories.to_objc());
|
||||
panel.setCanChooseFiles_(options.files.to_objc());
|
||||
panel.setAllowsMultipleSelection_(options.multiple.to_objc());
|
||||
panel.setResolvesAliases_(false.to_objc());
|
||||
let done_tx = Cell::new(Some(done_tx));
|
||||
let block = ConcreteBlock::new(move |response: NSModalResponse| {
|
||||
let result = if response == NSModalResponse::NSModalResponseOk {
|
||||
let mut result = Vec::new();
|
||||
let urls = panel.URLs();
|
||||
for i in 0..urls.count() {
|
||||
let url = urls.objectAtIndex(i);
|
||||
if url.isFileURL() == YES {
|
||||
if let Ok(path) = ns_url_to_path(url) {
|
||||
result.push(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(result)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Some(result)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(done_tx) = done_tx.take() {
|
||||
let _ = done_tx.send(result);
|
||||
if let Some(done_tx) = done_tx.take() {
|
||||
let _ = done_tx.send(result);
|
||||
}
|
||||
});
|
||||
let block = block.copy();
|
||||
let _: () = msg_send![panel, beginWithCompletionHandler: block];
|
||||
}
|
||||
});
|
||||
let block = block.copy();
|
||||
let _: () = msg_send![panel, beginWithCompletionHandler: block];
|
||||
done_rx
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
done_rx
|
||||
}
|
||||
|
||||
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
|
||||
unsafe {
|
||||
let panel = NSSavePanel::savePanel(nil);
|
||||
let path = ns_string(directory.to_string_lossy().as_ref());
|
||||
let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc());
|
||||
panel.setDirectoryURL(url);
|
||||
let directory = directory.to_owned();
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
self.foreground_executor()
|
||||
.spawn(async move {
|
||||
unsafe {
|
||||
let panel = NSSavePanel::savePanel(nil);
|
||||
let path = ns_string(directory.to_string_lossy().as_ref());
|
||||
let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc());
|
||||
panel.setDirectoryURL(url);
|
||||
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
let done_tx = Cell::new(Some(done_tx));
|
||||
let block = ConcreteBlock::new(move |response: NSModalResponse| {
|
||||
let mut result = None;
|
||||
if response == NSModalResponse::NSModalResponseOk {
|
||||
let url = panel.URL();
|
||||
if url.isFileURL() == YES {
|
||||
result = ns_url_to_path(panel.URL()).ok()
|
||||
}
|
||||
}
|
||||
let done_tx = Cell::new(Some(done_tx));
|
||||
let block = ConcreteBlock::new(move |response: NSModalResponse| {
|
||||
let mut result = None;
|
||||
if response == NSModalResponse::NSModalResponseOk {
|
||||
let url = panel.URL();
|
||||
if url.isFileURL() == YES {
|
||||
result = ns_url_to_path(panel.URL()).ok()
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(done_tx) = done_tx.take() {
|
||||
let _ = done_tx.send(result);
|
||||
if let Some(done_tx) = done_tx.take() {
|
||||
let _ = done_tx.send(result);
|
||||
}
|
||||
});
|
||||
let block = block.copy();
|
||||
let _: () = msg_send![panel, beginWithCompletionHandler: block];
|
||||
}
|
||||
});
|
||||
let block = block.copy();
|
||||
let _: () = msg_send![panel, beginWithCompletionHandler: block];
|
||||
done_rx
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
done_rx
|
||||
}
|
||||
|
||||
fn reveal_path(&self, path: &Path) {
|
||||
|
||||
@@ -61,6 +61,16 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
|
||||
constant Quad *quads
|
||||
[[buffer(QuadInputIndex_Quads)]]) {
|
||||
Quad quad = quads[input.quad_id];
|
||||
|
||||
// Fast path when the quad is not rounded and doesn't have any border.
|
||||
if (quad.corner_radii.top_left == 0. && quad.corner_radii.bottom_left == 0. &&
|
||||
quad.corner_radii.top_right == 0. &&
|
||||
quad.corner_radii.bottom_right == 0. && quad.border_widths.top == 0. &&
|
||||
quad.border_widths.left == 0. && quad.border_widths.right == 0. &&
|
||||
quad.border_widths.bottom == 0.) {
|
||||
return input.background_color;
|
||||
}
|
||||
|
||||
float2 half_size =
|
||||
float2(quad.bounds.size.width, quad.bounds.size.height) / 2.;
|
||||
float2 center =
|
||||
|
||||
@@ -10,6 +10,7 @@ use core_foundation::{
|
||||
array::CFIndex,
|
||||
attributed_string::{CFAttributedStringRef, CFMutableAttributedString},
|
||||
base::{CFRange, TCFType},
|
||||
number::CFNumber,
|
||||
string::CFString,
|
||||
};
|
||||
use core_graphics::{
|
||||
@@ -17,7 +18,14 @@ use core_graphics::{
|
||||
color_space::CGColorSpace,
|
||||
context::CGContext,
|
||||
};
|
||||
use core_text::{font::CTFont, line::CTLine, string_attributes::kCTFontAttributeName};
|
||||
use core_text::{
|
||||
font::CTFont,
|
||||
font_descriptor::{
|
||||
kCTFontSlantTrait, kCTFontSymbolicTrait, kCTFontWeightTrait, kCTFontWidthTrait,
|
||||
},
|
||||
line::CTLine,
|
||||
string_attributes::kCTFontAttributeName,
|
||||
};
|
||||
use font_kit::{
|
||||
font::Font as FontKitFont,
|
||||
handle::Handle,
|
||||
@@ -208,6 +216,35 @@ impl MacTextSystemState {
|
||||
let Some(_) = font.glyph_for_char('m') else {
|
||||
continue;
|
||||
};
|
||||
// We've seen a number of panics in production caused by calling font.properties()
|
||||
// which unwraps a downcast to CFNumber. This is an attempt to avoid the panic,
|
||||
// and to try and identify the incalcitrant font.
|
||||
let traits = font.native_font().all_traits();
|
||||
if unsafe {
|
||||
!(traits
|
||||
.get(kCTFontSymbolicTrait)
|
||||
.downcast::<CFNumber>()
|
||||
.is_some()
|
||||
&& traits
|
||||
.get(kCTFontWidthTrait)
|
||||
.downcast::<CFNumber>()
|
||||
.is_some()
|
||||
&& traits
|
||||
.get(kCTFontWeightTrait)
|
||||
.downcast::<CFNumber>()
|
||||
.is_some()
|
||||
&& traits
|
||||
.get(kCTFontSlantTrait)
|
||||
.downcast::<CFNumber>()
|
||||
.is_some())
|
||||
} {
|
||||
log::error!(
|
||||
"Failed to read traits for font {:?}",
|
||||
font.postscript_name().unwrap()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let font_id = FontId(self.fonts.len());
|
||||
font_ids.push(font_id);
|
||||
let postscript_name = font.postscript_name().unwrap();
|
||||
|
||||
@@ -772,7 +772,13 @@ impl PlatformWindow for MacWindow {
|
||||
self.0.as_ref().lock().input_handler.take()
|
||||
}
|
||||
|
||||
fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver<usize> {
|
||||
fn prompt(
|
||||
&self,
|
||||
level: PromptLevel,
|
||||
msg: &str,
|
||||
detail: Option<&str>,
|
||||
answers: &[&str],
|
||||
) -> oneshot::Receiver<usize> {
|
||||
// macOs applies overrides to modal window buttons after they are added.
|
||||
// Two most important for this logic are:
|
||||
// * Buttons with "Cancel" title will be displayed as the last buttons in the modal
|
||||
@@ -808,6 +814,9 @@ impl PlatformWindow for MacWindow {
|
||||
};
|
||||
let _: () = msg_send![alert, setAlertStyle: alert_style];
|
||||
let _: () = msg_send![alert, setMessageText: ns_string(msg)];
|
||||
if let Some(detail) = detail {
|
||||
let _: () = msg_send![alert, setInformativeText: ns_string(detail)];
|
||||
}
|
||||
|
||||
for (ix, answer) in answers
|
||||
.iter()
|
||||
|
||||
@@ -185,6 +185,7 @@ impl PlatformWindow for TestWindow {
|
||||
&self,
|
||||
_level: crate::PromptLevel,
|
||||
_msg: &str,
|
||||
_detail: Option<&str>,
|
||||
_answers: &[&str],
|
||||
) -> futures::channel::oneshot::Receiver<usize> {
|
||||
self.0
|
||||
|
||||
@@ -328,7 +328,13 @@ impl Element for AnyView {
|
||||
element.draw(bounds.origin, bounds.size.into(), cx);
|
||||
}
|
||||
|
||||
state.next_stacking_order_id = cx.window.next_frame.next_stacking_order_id;
|
||||
state.next_stacking_order_id = cx
|
||||
.window
|
||||
.next_frame
|
||||
.next_stacking_order_ids
|
||||
.last()
|
||||
.copied()
|
||||
.unwrap();
|
||||
state.cache_key = Some(ViewCacheKey {
|
||||
bounds,
|
||||
stacking_order: cx.stacking_order().clone(),
|
||||
|
||||
@@ -94,7 +94,6 @@ type AnyObserver = Box<dyn FnMut(&mut WindowContext) -> bool + 'static>;
|
||||
|
||||
type AnyWindowFocusListener = Box<dyn FnMut(&FocusEvent, &mut WindowContext) -> bool + 'static>;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct FocusEvent {
|
||||
previous_focus_path: SmallVec<[FocusId; 8]>,
|
||||
current_focus_path: SmallVec<[FocusId; 8]>,
|
||||
@@ -860,15 +859,14 @@ impl<'a> WindowContext<'a> {
|
||||
|
||||
// At this point, we've established that this opaque layer is on top of the queried layer
|
||||
// and contains the position:
|
||||
// - If the opaque layer is an extension of the queried layer, we don't want
|
||||
// to consider the opaque layer to be on top and so we ignore it.
|
||||
// - Else, we will bail early and say that the queried layer wasn't the top one.
|
||||
let opaque_layer_is_extension_of_queried_layer = opaque_layer.len() >= layer.len()
|
||||
&& opaque_layer
|
||||
.iter()
|
||||
.zip(layer.iter())
|
||||
.all(|(a, b)| a.z_index == b.z_index);
|
||||
if !opaque_layer_is_extension_of_queried_layer {
|
||||
// If neither the opaque layer or the queried layer is an extension of the other then
|
||||
// we know they are on different stacking orders, and return false.
|
||||
let is_on_same_layer = opaque_layer
|
||||
.iter()
|
||||
.zip(layer.iter())
|
||||
.all(|(a, b)| a.z_index == b.z_index);
|
||||
|
||||
if !is_on_same_layer {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -909,15 +907,14 @@ impl<'a> WindowContext<'a> {
|
||||
|
||||
// At this point, we've established that this opaque layer is on top of the queried layer
|
||||
// and contains the position:
|
||||
// - If the opaque layer is an extension of the queried layer, we don't want
|
||||
// to consider the opaque layer to be on top and so we ignore it.
|
||||
// - Else, we will bail early and say that the queried layer wasn't the top one.
|
||||
let opaque_layer_is_extension_of_queried_layer = opaque_layer.len() >= layer.len()
|
||||
&& opaque_layer
|
||||
.iter()
|
||||
.zip(layer.iter())
|
||||
.all(|(a, b)| a.z_index == b.z_index);
|
||||
if !opaque_layer_is_extension_of_queried_layer {
|
||||
// If neither the opaque layer or the queried layer is an extension of the other then
|
||||
// we know they are on different stacking orders, and return false.
|
||||
let is_on_same_layer = opaque_layer
|
||||
.iter()
|
||||
.zip(layer.iter())
|
||||
.all(|(a, b)| a.z_index == b.z_index);
|
||||
|
||||
if !is_on_same_layer {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1479,9 +1476,12 @@ impl<'a> WindowContext<'a> {
|
||||
&self,
|
||||
level: PromptLevel,
|
||||
message: &str,
|
||||
detail: Option<&str>,
|
||||
answers: &[&str],
|
||||
) -> oneshot::Receiver<usize> {
|
||||
self.window.platform_window.prompt(level, message, answers)
|
||||
self.window
|
||||
.platform_window
|
||||
.prompt(level, message, detail, answers)
|
||||
}
|
||||
|
||||
/// Returns all available actions for the focused element.
|
||||
@@ -2022,12 +2022,11 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||
}
|
||||
}
|
||||
|
||||
// Always emit a notify effect, so that handlers fire correctly
|
||||
self.window_cx.app.push_effect(Effect::Notify {
|
||||
emitter: self.view.model.entity_id,
|
||||
});
|
||||
if !self.window.drawing {
|
||||
self.window_cx.window.dirty = true;
|
||||
self.window_cx.app.push_effect(Effect::Notify {
|
||||
emitter: self.view.model.entity_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2759,59 +2758,3 @@ pub fn outline(bounds: impl Into<Bounds<Pixels>>, border_color: impl Into<Hsla>)
|
||||
border_color: border_color.into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
self as gpui, div, FocusHandle, InteractiveElement, IntoElement, Render, TestAppContext,
|
||||
ViewContext, VisualContext,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
fn test_notify_on_focus(cx: &mut TestAppContext) {
|
||||
struct TestFocusView {
|
||||
handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl Render for TestFocusView {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div().id("test").track_focus(&self.handle)
|
||||
}
|
||||
}
|
||||
|
||||
let notify_counter = Rc::new(RefCell::new(0));
|
||||
|
||||
let (notify_producer, cx) = cx.add_window_view(|cx| {
|
||||
cx.activate_window();
|
||||
let handle = cx.focus_handle();
|
||||
|
||||
cx.on_focus(&handle, |_, cx| {
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
|
||||
TestFocusView { handle }
|
||||
});
|
||||
|
||||
let focus_handle = cx.update(|cx| notify_producer.read(cx).handle.clone());
|
||||
|
||||
let _notify_consumer = cx.new_view({
|
||||
|cx| {
|
||||
let notify_counter = notify_counter.clone();
|
||||
cx.observe(¬ify_producer, move |_, _, _| {
|
||||
*notify_counter.borrow_mut() += 1;
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.focus(&focus_handle);
|
||||
});
|
||||
|
||||
assert_eq!(*notify_counter.borrow(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ pub(crate) struct Frame {
|
||||
pub(crate) scene: Scene,
|
||||
pub(crate) depth_map: Vec<(StackingOrder, EntityId, Bounds<Pixels>)>,
|
||||
pub(crate) z_index_stack: StackingOrder,
|
||||
pub(crate) next_stacking_order_id: u16,
|
||||
pub(crate) next_stacking_order_ids: Vec<u16>,
|
||||
pub(crate) next_root_z_index: u16,
|
||||
pub(crate) content_mask_stack: Vec<ContentMask<Pixels>>,
|
||||
pub(crate) element_offset_stack: Vec<Point<Pixels>>,
|
||||
@@ -85,7 +85,7 @@ impl Frame {
|
||||
scene: Scene::default(),
|
||||
depth_map: Vec::new(),
|
||||
z_index_stack: StackingOrder::default(),
|
||||
next_stacking_order_id: 0,
|
||||
next_stacking_order_ids: vec![0],
|
||||
next_root_z_index: 0,
|
||||
content_mask_stack: Vec::new(),
|
||||
element_offset_stack: Vec::new(),
|
||||
@@ -106,7 +106,7 @@ impl Frame {
|
||||
self.mouse_listeners.values_mut().for_each(Vec::clear);
|
||||
self.dispatch_tree.clear();
|
||||
self.depth_map.clear();
|
||||
self.next_stacking_order_id = 0;
|
||||
self.next_stacking_order_ids = vec![0];
|
||||
self.next_root_z_index = 0;
|
||||
self.reused_views.clear();
|
||||
self.scene.clear();
|
||||
@@ -351,8 +351,22 @@ impl<'a> ElementContext<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
debug_assert!(next_stacking_order_id >= self.window.next_frame.next_stacking_order_id);
|
||||
self.window.next_frame.next_stacking_order_id = next_stacking_order_id;
|
||||
debug_assert!(
|
||||
next_stacking_order_id
|
||||
>= self
|
||||
.window
|
||||
.next_frame
|
||||
.next_stacking_order_ids
|
||||
.last()
|
||||
.copied()
|
||||
.unwrap()
|
||||
);
|
||||
*self
|
||||
.window
|
||||
.next_frame
|
||||
.next_stacking_order_ids
|
||||
.last_mut()
|
||||
.unwrap() = next_stacking_order_id;
|
||||
}
|
||||
|
||||
/// Push a text style onto the stack, and call a function with that style active.
|
||||
@@ -434,8 +448,13 @@ impl<'a> ElementContext<'a> {
|
||||
};
|
||||
|
||||
let new_root_z_index = post_inc(&mut self.window_mut().next_frame.next_root_z_index);
|
||||
let new_stacking_order_id =
|
||||
post_inc(&mut self.window_mut().next_frame.next_stacking_order_id);
|
||||
let new_stacking_order_id = post_inc(
|
||||
self.window_mut()
|
||||
.next_frame
|
||||
.next_stacking_order_ids
|
||||
.last_mut()
|
||||
.unwrap(),
|
||||
);
|
||||
let new_context = StackingContext {
|
||||
z_index: new_root_z_index,
|
||||
id: new_stacking_order_id,
|
||||
@@ -455,8 +474,14 @@ impl<'a> ElementContext<'a> {
|
||||
/// Called during painting to invoke the given closure in a new stacking context. The given
|
||||
/// z-index is interpreted relative to the previous call to `stack`.
|
||||
pub fn with_z_index<R>(&mut self, z_index: u16, f: impl FnOnce(&mut Self) -> R) -> R {
|
||||
let new_stacking_order_id =
|
||||
post_inc(&mut self.window_mut().next_frame.next_stacking_order_id);
|
||||
let new_stacking_order_id = post_inc(
|
||||
self.window_mut()
|
||||
.next_frame
|
||||
.next_stacking_order_ids
|
||||
.last_mut()
|
||||
.unwrap(),
|
||||
);
|
||||
self.window_mut().next_frame.next_stacking_order_ids.push(0);
|
||||
let new_context = StackingContext {
|
||||
z_index,
|
||||
id: new_stacking_order_id,
|
||||
@@ -466,6 +491,8 @@ impl<'a> ElementContext<'a> {
|
||||
let result = f(self);
|
||||
self.window_mut().next_frame.z_index_stack.pop();
|
||||
|
||||
self.window_mut().next_frame.next_stacking_order_ids.pop();
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
|
||||
@@ -194,12 +194,14 @@ impl AsRef<Path> for RepositoryWorkDirectory {
|
||||
pub struct WorkDirectoryEntry(ProjectEntryId);
|
||||
|
||||
impl WorkDirectoryEntry {
|
||||
pub(crate) fn relativize(&self, worktree: &Snapshot, path: &Path) -> Option<RepoPath> {
|
||||
worktree.entry_for_id(self.0).and_then(|entry| {
|
||||
path.strip_prefix(&entry.path)
|
||||
.ok()
|
||||
.map(move |path| path.into())
|
||||
})
|
||||
pub(crate) fn relativize(&self, worktree: &Snapshot, path: &Path) -> Result<RepoPath> {
|
||||
let entry = worktree
|
||||
.entry_for_id(self.0)
|
||||
.ok_or_else(|| anyhow!("entry not found"))?;
|
||||
let path = path
|
||||
.strip_prefix(&entry.path)
|
||||
.map_err(|_| anyhow!("could not relativize {:?} against {:?}", path, entry.path))?;
|
||||
Ok(path.into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -970,13 +972,15 @@ impl LocalWorktree {
|
||||
let mut index_task = None;
|
||||
let snapshot = this.update(&mut cx, |this, _| this.as_local().unwrap().snapshot())?;
|
||||
if let Some(repo) = snapshot.repository_for_path(&path) {
|
||||
let repo_path = repo.work_directory.relativize(&snapshot, &path).unwrap();
|
||||
if let Some(repo) = snapshot.git_repositories.get(&*repo.work_directory) {
|
||||
let repo = repo.repo_ptr.clone();
|
||||
index_task = Some(
|
||||
cx.background_executor()
|
||||
.spawn(async move { repo.lock().load_index_text(&repo_path) }),
|
||||
);
|
||||
if let Some(repo_path) = repo.work_directory.relativize(&snapshot, &path).log_err()
|
||||
{
|
||||
if let Some(repo) = snapshot.git_repositories.get(&*repo.work_directory) {
|
||||
let repo = repo.repo_ptr.clone();
|
||||
index_task = Some(
|
||||
cx.background_executor()
|
||||
.spawn(async move { repo.lock().load_index_text(&repo_path) }),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3863,7 +3867,7 @@ impl BackgroundScanner {
|
||||
fs_entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, is_dir);
|
||||
fs_entry.is_external = !canonical_path.starts_with(&root_canonical_path);
|
||||
|
||||
if !is_dir && !fs_entry.is_ignored {
|
||||
if !is_dir && !fs_entry.is_ignored && !fs_entry.is_external {
|
||||
if let Some((work_dir, repo)) = state.snapshot.local_repo_for_path(path) {
|
||||
if let Ok(repo_path) = path.strip_prefix(work_dir.0) {
|
||||
let repo_path = RepoPath(repo_path.into());
|
||||
|
||||
@@ -781,6 +781,7 @@ impl ProjectPanel {
|
||||
let answer = cx.prompt(
|
||||
PromptLevel::Info,
|
||||
&format!("Delete {file_name:?}?"),
|
||||
None,
|
||||
&["Delete", "Cancel"],
|
||||
);
|
||||
|
||||
@@ -1370,7 +1371,7 @@ impl ProjectPanel {
|
||||
entry_id: *entry_id,
|
||||
})
|
||||
})
|
||||
.drag_over::<ProjectEntryId>(|style| {
|
||||
.drag_over::<ProjectEntryId>(|style, _, cx| {
|
||||
style.bg(cx.theme().colors().drop_target_background)
|
||||
})
|
||||
.on_drop(cx.listener(move |this, dragged_id: &ProjectEntryId, cx| {
|
||||
|
||||
@@ -181,7 +181,9 @@ message Envelope {
|
||||
MarkNotificationRead mark_notification_read = 153;
|
||||
LspExtExpandMacro lsp_ext_expand_macro = 154;
|
||||
LspExtExpandMacroResponse lsp_ext_expand_macro_response = 155;
|
||||
SetRoomParticipantRole set_room_participant_role = 156; // Current max
|
||||
SetRoomParticipantRole set_room_participant_role = 156;
|
||||
|
||||
UpdateUserChannels update_user_channels = 157; // current max
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,6 +199,23 @@ message Ack {}
|
||||
|
||||
message Error {
|
||||
string message = 1;
|
||||
ErrorCode code = 2;
|
||||
repeated string tags = 3;
|
||||
}
|
||||
|
||||
enum ErrorCode {
|
||||
Internal = 0;
|
||||
NoSuchChannel = 1;
|
||||
Disconnected = 2;
|
||||
SignedOut = 3;
|
||||
UpgradeRequired = 4;
|
||||
Forbidden = 5;
|
||||
WrongReleaseChannel = 6;
|
||||
NeedsCla = 7;
|
||||
NotARootChannel = 8;
|
||||
BadPublicNesting = 9;
|
||||
CircularNesting = 10;
|
||||
WrongMoveTarget = 11;
|
||||
}
|
||||
|
||||
message Test {
|
||||
@@ -979,21 +998,26 @@ message UpdateChannels {
|
||||
repeated Channel channel_invitations = 5;
|
||||
repeated uint64 remove_channel_invitations = 6;
|
||||
repeated ChannelParticipants channel_participants = 7;
|
||||
repeated UnseenChannelMessage unseen_channel_messages = 9;
|
||||
repeated UnseenChannelBufferChange unseen_channel_buffer_changes = 10;
|
||||
repeated ChannelMessageId latest_channel_message_ids = 8;
|
||||
repeated ChannelBufferVersion latest_channel_buffer_versions = 9;
|
||||
}
|
||||
|
||||
message UnseenChannelMessage {
|
||||
message UpdateUserChannels {
|
||||
repeated ChannelMessageId observed_channel_message_id = 1;
|
||||
repeated ChannelBufferVersion observed_channel_buffer_version = 2;
|
||||
repeated ChannelMembership channel_memberships = 3;
|
||||
}
|
||||
|
||||
message ChannelMembership {
|
||||
uint64 channel_id = 1;
|
||||
ChannelRole role = 2;
|
||||
}
|
||||
|
||||
message ChannelMessageId {
|
||||
uint64 channel_id = 1;
|
||||
uint64 message_id = 2;
|
||||
}
|
||||
|
||||
message UnseenChannelBufferChange {
|
||||
uint64 channel_id = 1;
|
||||
uint64 epoch = 2;
|
||||
repeated VectorClockEntry version = 3;
|
||||
}
|
||||
|
||||
message ChannelPermission {
|
||||
uint64 channel_id = 1;
|
||||
ChannelRole role = 3;
|
||||
@@ -1028,7 +1052,6 @@ message ChannelMember {
|
||||
enum Kind {
|
||||
Member = 0;
|
||||
Invitee = 1;
|
||||
AncestorMember = 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1135,7 +1158,7 @@ message GetChannelMessagesById {
|
||||
|
||||
message MoveChannel {
|
||||
uint64 channel_id = 1;
|
||||
optional uint64 to = 2;
|
||||
uint64 to = 2;
|
||||
}
|
||||
|
||||
message JoinChannelBuffer {
|
||||
@@ -1573,7 +1596,6 @@ message Channel {
|
||||
uint64 id = 1;
|
||||
string name = 2;
|
||||
ChannelVisibility visibility = 3;
|
||||
ChannelRole role = 4;
|
||||
repeated uint64 parent_path = 5;
|
||||
}
|
||||
|
||||
|
||||
223
crates/rpc/src/error.rs
Normal file
223
crates/rpc/src/error.rs
Normal file
@@ -0,0 +1,223 @@
|
||||
/// Some helpers for structured error handling.
|
||||
///
|
||||
/// The helpers defined here allow you to pass type-safe error codes from
|
||||
/// the collab server to the client; and provide a mechanism for additional
|
||||
/// structured data alongside the message.
|
||||
///
|
||||
/// When returning an error, it can be as simple as:
|
||||
///
|
||||
/// `return Err(Error::Forbidden.into())`
|
||||
///
|
||||
/// If you'd like to log more context, you can set a message. These messages
|
||||
/// show up in our logs, but are not shown visibly to users.
|
||||
///
|
||||
/// `return Err(Error::Forbidden.message("not an admin").into())`
|
||||
///
|
||||
/// If you'd like to provide enough context that the UI can render a good error
|
||||
/// message (or would be helpful to see in a structured format in the logs), you
|
||||
/// can use .with_tag():
|
||||
///
|
||||
/// `return Err(Error::WrongReleaseChannel.with_tag("required", "stable").into())`
|
||||
///
|
||||
/// When handling an error you can use .error_code() to match which error it was
|
||||
/// and .error_tag() to read any tags.
|
||||
///
|
||||
/// ```
|
||||
/// match err.error_code() {
|
||||
/// ErrorCode::Forbidden => alert("I'm sorry I can't do that.")
|
||||
/// ErrorCode::WrongReleaseChannel =>
|
||||
/// alert(format!("You need to be on the {} release channel.", err.error_tag("required").unwrap()))
|
||||
/// ErrorCode::Internal => alert("Sorry, something went wrong")
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
use crate::proto;
|
||||
pub use proto::ErrorCode;
|
||||
|
||||
/// ErrorCodeExt provides some helpers for structured error handling.
|
||||
///
|
||||
/// The primary implementation is on the proto::ErrorCode to easily convert
|
||||
/// that into an anyhow::Error, which we use pervasively.
|
||||
///
|
||||
/// The RpcError struct provides support for further metadata if needed.
|
||||
pub trait ErrorCodeExt {
|
||||
/// Return an anyhow::Error containing this.
|
||||
/// (useful in places where .into() doesn't have enough type information)
|
||||
fn anyhow(self) -> anyhow::Error;
|
||||
|
||||
/// Add a message to the error (by default the error code is used)
|
||||
fn message(self, msg: String) -> RpcError;
|
||||
|
||||
/// Add a tag to the error. Tags are key value pairs that can be used
|
||||
/// to send semi-structured data along with the error.
|
||||
fn with_tag(self, k: &str, v: &str) -> RpcError;
|
||||
}
|
||||
|
||||
impl ErrorCodeExt for proto::ErrorCode {
|
||||
fn anyhow(self) -> anyhow::Error {
|
||||
self.into()
|
||||
}
|
||||
|
||||
fn message(self, msg: String) -> RpcError {
|
||||
let err: RpcError = self.into();
|
||||
err.message(msg)
|
||||
}
|
||||
|
||||
fn with_tag(self, k: &str, v: &str) -> RpcError {
|
||||
let err: RpcError = self.into();
|
||||
err.with_tag(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
/// ErrorExt provides helpers for structured error handling.
|
||||
///
|
||||
/// The primary implementation is on the anyhow::Error, which is
|
||||
/// what we use throughout our codebase. Though under the hood this
|
||||
pub trait ErrorExt {
|
||||
/// error_code() returns the ErrorCode (or ErrorCode::Internal if there is none)
|
||||
fn error_code(&self) -> proto::ErrorCode;
|
||||
/// error_tag() returns the value of the tag with the given key, if any.
|
||||
fn error_tag(&self, k: &str) -> Option<&str>;
|
||||
/// to_proto() converts the error into a proto::Error
|
||||
fn to_proto(&self) -> proto::Error;
|
||||
}
|
||||
|
||||
impl ErrorExt for anyhow::Error {
|
||||
fn error_code(&self) -> proto::ErrorCode {
|
||||
if let Some(rpc_error) = self.downcast_ref::<RpcError>() {
|
||||
rpc_error.code
|
||||
} else {
|
||||
proto::ErrorCode::Internal
|
||||
}
|
||||
}
|
||||
|
||||
fn error_tag(&self, k: &str) -> Option<&str> {
|
||||
if let Some(rpc_error) = self.downcast_ref::<RpcError>() {
|
||||
rpc_error.error_tag(k)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn to_proto(&self) -> proto::Error {
|
||||
if let Some(rpc_error) = self.downcast_ref::<RpcError>() {
|
||||
rpc_error.to_proto()
|
||||
} else {
|
||||
ErrorCode::Internal.message(format!("{}", self)).to_proto()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<proto::ErrorCode> for anyhow::Error {
|
||||
fn from(value: proto::ErrorCode) -> Self {
|
||||
RpcError {
|
||||
request: None,
|
||||
code: value,
|
||||
msg: format!("{:?}", value).to_string(),
|
||||
tags: Default::default(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RpcError {
|
||||
request: Option<String>,
|
||||
msg: String,
|
||||
code: proto::ErrorCode,
|
||||
tags: Vec<String>,
|
||||
}
|
||||
|
||||
/// RpcError is a structured error type that is returned by the collab server.
|
||||
/// In addition to a message, it lets you set a specific ErrorCode, and attach
|
||||
/// small amounts of metadata to help the client handle the error appropriately.
|
||||
///
|
||||
/// This struct is not typically used directly, as we pass anyhow::Error around
|
||||
/// in the app; however it is useful for chaining .message() and .with_tag() on
|
||||
/// ErrorCode.
|
||||
impl RpcError {
|
||||
/// from_proto converts a proto::Error into an anyhow::Error containing
|
||||
/// an RpcError.
|
||||
pub fn from_proto(error: &proto::Error, request: &str) -> anyhow::Error {
|
||||
RpcError {
|
||||
request: Some(request.to_string()),
|
||||
code: error.code(),
|
||||
msg: error.message.clone(),
|
||||
tags: error.tags.clone(),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorCodeExt for RpcError {
|
||||
fn message(mut self, msg: String) -> RpcError {
|
||||
self.msg = msg;
|
||||
self
|
||||
}
|
||||
|
||||
fn with_tag(mut self, k: &str, v: &str) -> RpcError {
|
||||
self.tags.push(format!("{}={}", k, v));
|
||||
self
|
||||
}
|
||||
|
||||
fn anyhow(self) -> anyhow::Error {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorExt for RpcError {
|
||||
fn error_tag(&self, k: &str) -> Option<&str> {
|
||||
for tag in &self.tags {
|
||||
let mut parts = tag.split('=');
|
||||
if let Some(key) = parts.next() {
|
||||
if key == k {
|
||||
return parts.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn error_code(&self) -> proto::ErrorCode {
|
||||
self.code
|
||||
}
|
||||
|
||||
fn to_proto(&self) -> proto::Error {
|
||||
proto::Error {
|
||||
code: self.code as i32,
|
||||
message: self.msg.clone(),
|
||||
tags: self.tags.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for RpcError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RpcError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
if let Some(request) = &self.request {
|
||||
write!(f, "RPC request {} failed: {}", request, self.msg)?
|
||||
} else {
|
||||
write!(f, "{}", self.msg)?
|
||||
}
|
||||
for tag in &self.tags {
|
||||
write!(f, " {}", tag)?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<proto::ErrorCode> for RpcError {
|
||||
fn from(code: proto::ErrorCode) -> Self {
|
||||
RpcError {
|
||||
request: None,
|
||||
code,
|
||||
msg: format!("{:?}", code).to_string(),
|
||||
tags: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
use crate::{ErrorCode, ErrorCodeExt, ErrorExt, RpcError};
|
||||
|
||||
use super::{
|
||||
proto::{self, AnyTypedEnvelope, EnvelopedMessage, MessageStream, PeerId, RequestMessage},
|
||||
Connection,
|
||||
@@ -423,11 +425,7 @@ impl Peer {
|
||||
let (response, _barrier) = rx.await.map_err(|_| anyhow!("connection was closed"))?;
|
||||
|
||||
if let Some(proto::envelope::Payload::Error(error)) = &response.payload {
|
||||
Err(anyhow!(
|
||||
"RPC request {} failed - {}",
|
||||
T::NAME,
|
||||
error.message
|
||||
))
|
||||
Err(RpcError::from_proto(&error, T::NAME))
|
||||
} else {
|
||||
Ok(TypedEnvelope {
|
||||
message_id: response.id,
|
||||
@@ -516,9 +514,12 @@ impl Peer {
|
||||
envelope: Box<dyn AnyTypedEnvelope>,
|
||||
) -> Result<()> {
|
||||
let connection = self.connection_state(envelope.sender_id())?;
|
||||
let response = proto::Error {
|
||||
message: format!("message {} was not handled", envelope.payload_type_name()),
|
||||
};
|
||||
let response = ErrorCode::Internal
|
||||
.message(format!(
|
||||
"message {} was not handled",
|
||||
envelope.payload_type_name()
|
||||
))
|
||||
.to_proto();
|
||||
let message_id = connection
|
||||
.next_message_id
|
||||
.fetch_add(1, atomic::Ordering::SeqCst);
|
||||
@@ -692,17 +693,17 @@ mod tests {
|
||||
server
|
||||
.send(
|
||||
server_to_client_conn_id,
|
||||
proto::Error {
|
||||
message: "message 1".to_string(),
|
||||
},
|
||||
ErrorCode::Internal
|
||||
.message("message 1".to_string())
|
||||
.to_proto(),
|
||||
)
|
||||
.unwrap();
|
||||
server
|
||||
.send(
|
||||
server_to_client_conn_id,
|
||||
proto::Error {
|
||||
message: "message 2".to_string(),
|
||||
},
|
||||
ErrorCode::Internal
|
||||
.message("message 2".to_string())
|
||||
.to_proto(),
|
||||
)
|
||||
.unwrap();
|
||||
server.respond(request.receipt(), proto::Ack {}).unwrap();
|
||||
@@ -797,17 +798,17 @@ mod tests {
|
||||
server
|
||||
.send(
|
||||
server_to_client_conn_id,
|
||||
proto::Error {
|
||||
message: "message 1".to_string(),
|
||||
},
|
||||
ErrorCode::Internal
|
||||
.message("message 1".to_string())
|
||||
.to_proto(),
|
||||
)
|
||||
.unwrap();
|
||||
server
|
||||
.send(
|
||||
server_to_client_conn_id,
|
||||
proto::Error {
|
||||
message: "message 2".to_string(),
|
||||
},
|
||||
ErrorCode::Internal
|
||||
.message("message 2".to_string())
|
||||
.to_proto(),
|
||||
)
|
||||
.unwrap();
|
||||
server.respond(request1.receipt(), proto::Ack {}).unwrap();
|
||||
|
||||
@@ -269,6 +269,7 @@ messages!(
|
||||
(UpdateChannelBuffer, Foreground),
|
||||
(UpdateChannelBufferCollaborators, Foreground),
|
||||
(UpdateChannels, Foreground),
|
||||
(UpdateUserChannels, Foreground),
|
||||
(UpdateContacts, Foreground),
|
||||
(UpdateDiagnosticSummary, Foreground),
|
||||
(UpdateDiffBase, Foreground),
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
pub mod auth;
|
||||
mod conn;
|
||||
mod error;
|
||||
mod notification;
|
||||
mod peer;
|
||||
pub mod proto;
|
||||
|
||||
pub use conn::Connection;
|
||||
pub use error::*;
|
||||
pub use notification::*;
|
||||
pub use peer::*;
|
||||
mod macros;
|
||||
|
||||
pub const PROTOCOL_VERSION: u32 = 67;
|
||||
pub const PROTOCOL_VERSION: u32 = 68;
|
||||
|
||||
@@ -746,6 +746,7 @@ impl ProjectSearchView {
|
||||
cx.prompt(
|
||||
PromptLevel::Info,
|
||||
prompt_text.as_str(),
|
||||
None,
|
||||
&["Continue", "Cancel"],
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -18,7 +18,7 @@ db = { path = "../db" }
|
||||
theme = { path = "../theme" }
|
||||
util = { path = "../util" }
|
||||
|
||||
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "33306142195b354ef3485ca2b1d8a85dfc6605ca" }
|
||||
alacritty_terminal = "0.21"
|
||||
procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false }
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use alacritty_terminal::term::color::Rgb as AlacRgb;
|
||||
|
||||
use alacritty_terminal::vte::ansi::Rgb as AlacRgb;
|
||||
use gpui::Rgba;
|
||||
|
||||
//Convenience method to convert from a GPUI color to an alacritty Rgb
|
||||
@@ -8,5 +7,5 @@ pub fn to_alac_rgb(color: impl Into<Rgba>) -> AlacRgb {
|
||||
let r = ((color.r * color.a) * 255.) as u8;
|
||||
let g = ((color.g * color.a) * 255.) as u8;
|
||||
let b = ((color.b * color.a) * 255.) as u8;
|
||||
AlacRgb::new(r, g, b)
|
||||
AlacRgb { r, g, b }
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ pub use alacritty_terminal;
|
||||
pub mod terminal_settings;
|
||||
|
||||
use alacritty_terminal::{
|
||||
ansi::{ClearMode, Handler},
|
||||
config::{Config, Program, PtyConfig, Scrolling},
|
||||
event::{Event as AlacTermEvent, EventListener, Notify, WindowSize},
|
||||
event_loop::{EventLoop, Msg, Notifier},
|
||||
grid::{Dimensions, Scroll as AlacScroll},
|
||||
@@ -13,11 +11,11 @@ use alacritty_terminal::{
|
||||
sync::FairMutex,
|
||||
term::{
|
||||
cell::Cell,
|
||||
color::Rgb,
|
||||
search::{Match, RegexIter, RegexSearch},
|
||||
RenderableCursor, TermMode,
|
||||
Config, RenderableCursor, TermMode,
|
||||
},
|
||||
tty::{self, setup_env},
|
||||
vte::ansi::{ClearMode, Handler, NamedPrivateMode, PrivateMode, Rgb},
|
||||
Term,
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
@@ -58,7 +56,6 @@ use gpui::{
|
||||
};
|
||||
|
||||
use crate::mappings::{colors::to_alac_rgb, keys::to_esc_str};
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
actions!(
|
||||
terminal,
|
||||
@@ -75,15 +72,6 @@ const DEBUG_TERMINAL_HEIGHT: Pixels = px(30.);
|
||||
const DEBUG_CELL_WIDTH: Pixels = px(5.);
|
||||
const DEBUG_LINE_HEIGHT: Pixels = px(5.);
|
||||
|
||||
lazy_static! {
|
||||
// Regex Copied from alacritty's ui_config.rs and modified its declaration slightly:
|
||||
// * avoid Rust-specific escaping.
|
||||
// * use more strict regex for `file://` protocol matching: original regex has `file:` inside, but we want to avoid matching `some::file::module` strings.
|
||||
static ref URL_REGEX: RegexSearch = RegexSearch::new(r#"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file://|git://|ssh:|ftp://)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>"\s{-}\^⟨⟩`]+"#).unwrap();
|
||||
|
||||
static ref WORD_REGEX: RegexSearch = RegexSearch::new(r#"[\w.\[\]:/@\-~]+"#).unwrap();
|
||||
}
|
||||
|
||||
///Upward flowing events, for changing the title and such
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Event {
|
||||
@@ -289,66 +277,70 @@ impl TerminalBuilder {
|
||||
pub fn new(
|
||||
working_directory: Option<PathBuf>,
|
||||
shell: Shell,
|
||||
mut env: HashMap<String, String>,
|
||||
env: HashMap<String, String>,
|
||||
blink_settings: Option<TerminalBlink>,
|
||||
alternate_scroll: AlternateScroll,
|
||||
window: AnyWindowHandle,
|
||||
) -> Result<TerminalBuilder> {
|
||||
let pty_config = {
|
||||
let pty_options = {
|
||||
let alac_shell = match shell.clone() {
|
||||
Shell::System => None,
|
||||
Shell::Program(program) => Some(Program::Just(program)),
|
||||
Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }),
|
||||
Shell::Program(program) => {
|
||||
Some(alacritty_terminal::tty::Shell::new(program, Vec::new()))
|
||||
}
|
||||
Shell::WithArguments { program, args } => {
|
||||
Some(alacritty_terminal::tty::Shell::new(program, args))
|
||||
}
|
||||
};
|
||||
|
||||
PtyConfig {
|
||||
alacritty_terminal::tty::Options {
|
||||
shell: alac_shell,
|
||||
working_directory: working_directory.clone(),
|
||||
hold: false,
|
||||
}
|
||||
};
|
||||
|
||||
//TODO: Properly set the current locale,
|
||||
env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
|
||||
env.insert("ZED_TERM".to_string(), true.to_string());
|
||||
// First, setup Alacritty's env
|
||||
setup_env();
|
||||
|
||||
let alac_scrolling = Scrolling::default();
|
||||
// alac_scrolling.set_history((BACK_BUFFER_SIZE * 2) as u32);
|
||||
// Then setup configured environment variables
|
||||
for (key, value) in env {
|
||||
std::env::set_var(key, value);
|
||||
}
|
||||
//TODO: Properly set the current locale,
|
||||
std::env::set_var("LC_ALL", "en_US.UTF-8");
|
||||
std::env::set_var("ZED_TERM", "true");
|
||||
|
||||
let config = Config {
|
||||
pty_config: pty_config.clone(),
|
||||
env,
|
||||
scrolling: alac_scrolling,
|
||||
scrolling_history: 10000,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
setup_env(&config);
|
||||
|
||||
//Spawn a task so the Alacritty EventLoop can communicate with us in a view context
|
||||
//TODO: Remove with a bounded sender which can be dispatched on &self
|
||||
let (events_tx, events_rx) = unbounded();
|
||||
//Set up the terminal...
|
||||
let mut term = Term::new(
|
||||
&config,
|
||||
config,
|
||||
&TerminalSize::default(),
|
||||
ZedListener(events_tx.clone()),
|
||||
);
|
||||
|
||||
//Start off blinking if we need to
|
||||
if let Some(TerminalBlink::On) = blink_settings {
|
||||
term.set_mode(alacritty_terminal::ansi::Mode::BlinkingCursor)
|
||||
term.set_private_mode(PrivateMode::Named(NamedPrivateMode::BlinkingCursor));
|
||||
}
|
||||
|
||||
//Alacritty defaults to alternate scrolling being on, so we just need to turn it off.
|
||||
if let AlternateScroll::Off = alternate_scroll {
|
||||
term.unset_mode(alacritty_terminal::ansi::Mode::AlternateScroll)
|
||||
term.unset_private_mode(PrivateMode::Named(NamedPrivateMode::AlternateScroll));
|
||||
}
|
||||
|
||||
let term = Arc::new(FairMutex::new(term));
|
||||
|
||||
//Setup the pty...
|
||||
let pty = match tty::new(
|
||||
&pty_config,
|
||||
&pty_options,
|
||||
TerminalSize::default().into(),
|
||||
window.window_id().as_u64(),
|
||||
) {
|
||||
@@ -370,13 +362,16 @@ impl TerminalBuilder {
|
||||
term.clone(),
|
||||
ZedListener(events_tx.clone()),
|
||||
pty,
|
||||
pty_config.hold,
|
||||
pty_options.hold,
|
||||
false,
|
||||
);
|
||||
|
||||
//Kick things off
|
||||
let pty_tx = event_loop.channel();
|
||||
let _io_thread = event_loop.spawn();
|
||||
let _io_thread = event_loop.spawn(); // DANGER
|
||||
|
||||
let url_regex = RegexSearch::new(r#"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file://|git://|ssh:|ftp://)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>"\s{-}\^⟨⟩`]+"#).unwrap();
|
||||
let word_regex = RegexSearch::new(r#"[\w.\[\]:/@\-~]+"#).unwrap();
|
||||
|
||||
let terminal = Terminal {
|
||||
pty_tx: Notifier(pty_tx),
|
||||
@@ -396,6 +391,8 @@ impl TerminalBuilder {
|
||||
selection_phase: SelectionPhase::Ended,
|
||||
cmd_pressed: false,
|
||||
hovered_word: false,
|
||||
url_regex,
|
||||
word_regex,
|
||||
};
|
||||
|
||||
Ok(TerminalBuilder {
|
||||
@@ -514,7 +511,7 @@ impl Default for TerminalContent {
|
||||
selection_text: Default::default(),
|
||||
selection: Default::default(),
|
||||
cursor: RenderableCursor {
|
||||
shape: alacritty_terminal::ansi::CursorShape::Block,
|
||||
shape: alacritty_terminal::vte::ansi::CursorShape::Block,
|
||||
point: AlacPoint::new(Line(0), Column(0)),
|
||||
},
|
||||
cursor_char: Default::default(),
|
||||
@@ -550,6 +547,8 @@ pub struct Terminal {
|
||||
selection_phase: SelectionPhase,
|
||||
cmd_pressed: bool,
|
||||
hovered_word: bool,
|
||||
url_regex: RegexSearch,
|
||||
word_regex: RegexSearch,
|
||||
}
|
||||
|
||||
impl Terminal {
|
||||
@@ -760,7 +759,7 @@ impl Terminal {
|
||||
let url_match = min_index..=max_index;
|
||||
|
||||
Some((url, true, url_match))
|
||||
} else if let Some(word_match) = regex_match_at(term, point, &WORD_REGEX) {
|
||||
} else if let Some(word_match) = regex_match_at(term, point, &mut self.word_regex) {
|
||||
let maybe_url_or_path =
|
||||
term.bounds_to_string(*word_match.start(), *word_match.end());
|
||||
let original_match = word_match.clone();
|
||||
@@ -777,7 +776,7 @@ impl Terminal {
|
||||
(word_match, maybe_url_or_path)
|
||||
};
|
||||
|
||||
let is_url = match regex_match_at(term, point, &URL_REGEX) {
|
||||
let is_url = match regex_match_at(term, point, &mut self.url_regex) {
|
||||
Some(url_match) => {
|
||||
// `]` is a valid symbol in the `file://` URL, so the regex match will include it
|
||||
// consider that when ensuring that the URL match is the same as the original word
|
||||
@@ -1275,14 +1274,14 @@ impl Terminal {
|
||||
|
||||
pub fn find_matches(
|
||||
&mut self,
|
||||
searcher: RegexSearch,
|
||||
mut searcher: RegexSearch,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Vec<RangeInclusive<AlacPoint>>> {
|
||||
let term = self.term.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
let term = term.lock();
|
||||
|
||||
all_search_matches(&term, &searcher).collect()
|
||||
all_search_matches(&term, &mut searcher).collect()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1332,7 +1331,7 @@ impl EventEmitter<Event> for Terminal {}
|
||||
|
||||
/// Based on alacritty/src/display/hint.rs > regex_match_at
|
||||
/// Retrieve the match, if the specified point is inside the content matching the regex.
|
||||
fn regex_match_at<T>(term: &Term<T>, point: AlacPoint, regex: &RegexSearch) -> Option<Match> {
|
||||
fn regex_match_at<T>(term: &Term<T>, point: AlacPoint, regex: &mut RegexSearch) -> Option<Match> {
|
||||
visible_regex_match_iter(term, regex).find(|rm| rm.contains(&point))
|
||||
}
|
||||
|
||||
@@ -1340,7 +1339,7 @@ fn regex_match_at<T>(term: &Term<T>, point: AlacPoint, regex: &RegexSearch) -> O
|
||||
/// Iterate over all visible regex matches.
|
||||
pub fn visible_regex_match_iter<'a, T>(
|
||||
term: &'a Term<T>,
|
||||
regex: &'a RegexSearch,
|
||||
regex: &'a mut RegexSearch,
|
||||
) -> impl Iterator<Item = Match> + 'a {
|
||||
let viewport_start = Line(-(term.grid().display_offset() as i32));
|
||||
let viewport_end = viewport_start + term.bottommost_line();
|
||||
@@ -1362,7 +1361,7 @@ fn make_selection(range: &RangeInclusive<AlacPoint>) -> Selection {
|
||||
|
||||
fn all_search_matches<'a, T>(
|
||||
term: &'a Term<T>,
|
||||
regex: &'a RegexSearch,
|
||||
regex: &'a mut RegexSearch,
|
||||
) -> impl Iterator<Item = Match> + 'a {
|
||||
let start = AlacPoint::new(term.grid().topmost_line(), Column(0));
|
||||
let end = AlacPoint::new(term.grid().bottommost_line(), term.grid().last_column());
|
||||
|
||||
@@ -11,12 +11,11 @@ use itertools::Itertools;
|
||||
use language::CursorShape;
|
||||
use settings::Settings;
|
||||
use terminal::{
|
||||
alacritty_terminal::ansi::NamedColor,
|
||||
alacritty_terminal::{
|
||||
ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape},
|
||||
grid::Dimensions,
|
||||
index::Point as AlacPoint,
|
||||
term::{cell::Flags, TermMode},
|
||||
vte::ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor},
|
||||
},
|
||||
terminal_settings::TerminalSettings,
|
||||
IndexedCell, Terminal, TerminalContent, TerminalSize,
|
||||
@@ -308,7 +307,7 @@ impl TerminalElement {
|
||||
/// Converts the Alacritty cell styles to GPUI text styles and background color.
|
||||
fn cell_style(
|
||||
indexed: &IndexedCell,
|
||||
fg: terminal::alacritty_terminal::ansi::Color,
|
||||
fg: terminal::alacritty_terminal::vte::ansi::Color,
|
||||
// bg: terminal::alacritty_terminal::ansi::Color,
|
||||
colors: &Theme,
|
||||
text_style: &TextStyle,
|
||||
@@ -998,11 +997,11 @@ fn to_highlighted_range_lines(
|
||||
}
|
||||
|
||||
/// Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent.
|
||||
fn convert_color(fg: &terminal::alacritty_terminal::ansi::Color, theme: &Theme) -> Hsla {
|
||||
fn convert_color(fg: &terminal::alacritty_terminal::vte::ansi::Color, theme: &Theme) -> Hsla {
|
||||
let colors = theme.colors();
|
||||
match fg {
|
||||
// Named and theme defined colors
|
||||
terminal::alacritty_terminal::ansi::Color::Named(n) => match n {
|
||||
terminal::alacritty_terminal::vte::ansi::Color::Named(n) => match n {
|
||||
NamedColor::Black => colors.terminal_ansi_black,
|
||||
NamedColor::Red => colors.terminal_ansi_red,
|
||||
NamedColor::Green => colors.terminal_ansi_green,
|
||||
@@ -1034,11 +1033,11 @@ fn convert_color(fg: &terminal::alacritty_terminal::ansi::Color, theme: &Theme)
|
||||
NamedColor::DimForeground => colors.terminal_dim_foreground,
|
||||
},
|
||||
// 'True' colors
|
||||
terminal::alacritty_terminal::ansi::Color::Spec(rgb) => {
|
||||
terminal::alacritty_terminal::vte::ansi::Color::Spec(rgb) => {
|
||||
terminal::rgba_color(rgb.r, rgb.g, rgb.b)
|
||||
}
|
||||
// 8 bit, indexed colors
|
||||
terminal::alacritty_terminal::ansi::Color::Indexed(i) => {
|
||||
terminal::alacritty_terminal::vte::ansi::Color::Indexed(i) => {
|
||||
terminal::get_color_at_index(*i as usize, theme)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,7 +280,7 @@ fn generate_positions(string: &str, query: &str) -> Vec<usize> {
|
||||
return positions;
|
||||
};
|
||||
|
||||
for (i, c) in string.chars().enumerate() {
|
||||
for (i, c) in string.char_indices() {
|
||||
if c == current {
|
||||
positions.push(i);
|
||||
if let Some(c) = chars.next() {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{Toast, Workspace};
|
||||
use collections::HashMap;
|
||||
use gpui::{
|
||||
AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter, Render,
|
||||
Task, View, ViewContext, VisualContext, WindowContext,
|
||||
AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter,
|
||||
PromptLevel, Render, Task, View, ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
use std::{any::TypeId, ops::DerefMut};
|
||||
|
||||
@@ -299,7 +299,7 @@ pub trait NotifyTaskExt {
|
||||
|
||||
impl<R, E> NotifyTaskExt for Task<Result<R, E>>
|
||||
where
|
||||
E: std::fmt::Debug + 'static,
|
||||
E: std::fmt::Debug + Sized + 'static,
|
||||
R: 'static,
|
||||
{
|
||||
fn detach_and_notify_err(self, cx: &mut WindowContext) {
|
||||
@@ -307,3 +307,39 @@ where
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
pub trait DetachAndPromptErr {
|
||||
fn detach_and_prompt_err(
|
||||
self,
|
||||
msg: &str,
|
||||
cx: &mut WindowContext,
|
||||
f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
|
||||
);
|
||||
}
|
||||
|
||||
impl<R> DetachAndPromptErr for Task<anyhow::Result<R>>
|
||||
where
|
||||
R: 'static,
|
||||
{
|
||||
fn detach_and_prompt_err(
|
||||
self,
|
||||
msg: &str,
|
||||
cx: &mut WindowContext,
|
||||
f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
|
||||
) {
|
||||
let msg = msg.to_owned();
|
||||
cx.spawn(|mut cx| async move {
|
||||
if let Err(err) = self.await {
|
||||
log::error!("{err:?}");
|
||||
if let Ok(prompt) = cx.update(|cx| {
|
||||
let detail = f(&err, cx)
|
||||
.unwrap_or_else(|| format!("{err:?}. Please try again.", err = err));
|
||||
cx.prompt(PromptLevel::Critical, &msg, Some(&detail), &["Ok"])
|
||||
}) {
|
||||
prompt.await.ok();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -870,7 +870,7 @@ impl Pane {
|
||||
items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
|
||||
all_dirty_items: usize,
|
||||
cx: &AppContext,
|
||||
) -> String {
|
||||
) -> (String, String) {
|
||||
/// Quantity of item paths displayed in prompt prior to cutoff..
|
||||
const FILE_NAMES_CUTOFF_POINT: usize = 10;
|
||||
let mut file_names: Vec<_> = items
|
||||
@@ -894,10 +894,12 @@ impl Pane {
|
||||
file_names.push(format!(".. {} files not shown", not_shown_files).into());
|
||||
}
|
||||
}
|
||||
let file_names = file_names.join("\n");
|
||||
format!(
|
||||
"Do you want to save changes to the following {} files?\n{file_names}",
|
||||
all_dirty_items
|
||||
(
|
||||
format!(
|
||||
"Do you want to save changes to the following {} files?",
|
||||
all_dirty_items
|
||||
),
|
||||
file_names.join("\n"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -929,11 +931,12 @@ impl Pane {
|
||||
cx.spawn(|pane, mut cx| async move {
|
||||
if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
|
||||
let answer = pane.update(&mut cx, |_, cx| {
|
||||
let prompt =
|
||||
let (prompt, detail) =
|
||||
Self::file_names_for_prompt(&mut dirty_items.iter(), dirty_items.len(), cx);
|
||||
cx.prompt(
|
||||
PromptLevel::Warning,
|
||||
&prompt,
|
||||
Some(&detail),
|
||||
&["Save all", "Discard all", "Cancel"],
|
||||
)
|
||||
})?;
|
||||
@@ -1131,6 +1134,7 @@ impl Pane {
|
||||
cx.prompt(
|
||||
PromptLevel::Warning,
|
||||
CONFLICT_MESSAGE,
|
||||
None,
|
||||
&["Overwrite", "Discard", "Cancel"],
|
||||
)
|
||||
})?;
|
||||
@@ -1154,6 +1158,7 @@ impl Pane {
|
||||
cx.prompt(
|
||||
PromptLevel::Warning,
|
||||
&prompt,
|
||||
None,
|
||||
&["Save", "Don't Save", "Cancel"],
|
||||
)
|
||||
})?;
|
||||
@@ -1329,8 +1334,12 @@ impl Pane {
|
||||
},
|
||||
|tab, cx| cx.new_view(|_| tab.clone()),
|
||||
)
|
||||
.drag_over::<DraggedTab>(|tab| tab.bg(cx.theme().colors().drop_target_background))
|
||||
.drag_over::<ProjectEntryId>(|tab| tab.bg(cx.theme().colors().drop_target_background))
|
||||
.drag_over::<DraggedTab>(|tab, _, cx| {
|
||||
tab.bg(cx.theme().colors().drop_target_background)
|
||||
})
|
||||
.drag_over::<ProjectEntryId>(|tab, _, cx| {
|
||||
tab.bg(cx.theme().colors().drop_target_background)
|
||||
})
|
||||
.when_some(self.can_drop_predicate.clone(), |this, p| {
|
||||
this.can_drop(move |a, cx| p(a, cx))
|
||||
})
|
||||
@@ -1500,10 +1509,10 @@ impl Pane {
|
||||
.child("")
|
||||
.h_full()
|
||||
.flex_grow()
|
||||
.drag_over::<DraggedTab>(|bar| {
|
||||
.drag_over::<DraggedTab>(|bar, _, cx| {
|
||||
bar.bg(cx.theme().colors().drop_target_background)
|
||||
})
|
||||
.drag_over::<ProjectEntryId>(|bar| {
|
||||
.drag_over::<ProjectEntryId>(|bar, _, cx| {
|
||||
bar.bg(cx.theme().colors().drop_target_background)
|
||||
})
|
||||
.on_drop(cx.listener(move |this, dragged_tab: &DraggedTab, cx| {
|
||||
|
||||
@@ -14,8 +14,8 @@ mod workspace_settings;
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use call::ActiveCall;
|
||||
use client::{
|
||||
proto::{self, PeerId},
|
||||
Client, Status, TypedEnvelope, UserStore,
|
||||
proto::{self, ErrorCode, PeerId},
|
||||
Client, ErrorExt, Status, TypedEnvelope, UserStore,
|
||||
};
|
||||
use collections::{hash_map, HashMap, HashSet};
|
||||
use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
|
||||
@@ -30,8 +30,8 @@ use gpui::{
|
||||
DragMoveEvent, Element, ElementContext, Entity, EntityId, EventEmitter, FocusHandle,
|
||||
FocusableView, GlobalPixels, InteractiveElement, IntoElement, KeyContext, LayoutId,
|
||||
ManagedView, Model, ModelContext, ParentElement, PathPromptOptions, Pixels, Point, PromptLevel,
|
||||
Render, Size, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
|
||||
WindowBounds, WindowContext, WindowHandle, WindowOptions,
|
||||
Render, SharedString, Size, Styled, Subscription, Task, View, ViewContext, VisualContext,
|
||||
WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
|
||||
};
|
||||
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
|
||||
use itertools::Itertools;
|
||||
@@ -1159,6 +1159,7 @@ impl Workspace {
|
||||
cx.prompt(
|
||||
PromptLevel::Warning,
|
||||
"Do you want to leave the current call?",
|
||||
None,
|
||||
&["Close window and hang up", "Cancel"],
|
||||
)
|
||||
})?;
|
||||
@@ -1214,7 +1215,7 @@ impl Workspace {
|
||||
// Override save mode and display "Save all files" prompt
|
||||
if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
|
||||
let answer = workspace.update(&mut cx, |_, cx| {
|
||||
let prompt = Pane::file_names_for_prompt(
|
||||
let (prompt, detail) = Pane::file_names_for_prompt(
|
||||
&mut dirty_items.iter().map(|(_, handle)| handle),
|
||||
dirty_items.len(),
|
||||
cx,
|
||||
@@ -1222,6 +1223,7 @@ impl Workspace {
|
||||
cx.prompt(
|
||||
PromptLevel::Warning,
|
||||
&prompt,
|
||||
Some(&detail),
|
||||
&["Save all", "Discard all", "Cancel"],
|
||||
)
|
||||
})?;
|
||||
@@ -3887,13 +3889,16 @@ async fn join_channel_internal(
|
||||
|
||||
if should_prompt {
|
||||
if let Some(workspace) = requesting_window {
|
||||
let answer = workspace.update(cx, |_, cx| {
|
||||
cx.prompt(
|
||||
PromptLevel::Warning,
|
||||
"Leaving this call will unshare your current project.\nDo you want to switch channels?",
|
||||
&["Yes, Join Channel", "Cancel"],
|
||||
)
|
||||
})?.await;
|
||||
let answer = workspace
|
||||
.update(cx, |_, cx| {
|
||||
cx.prompt(
|
||||
PromptLevel::Warning,
|
||||
"Do you want to switch channels?",
|
||||
Some("Leaving this call will unshare your current project."),
|
||||
&["Yes, Join Channel", "Cancel"],
|
||||
)
|
||||
})?
|
||||
.await;
|
||||
|
||||
if answer == Ok(1) {
|
||||
return Ok(false);
|
||||
@@ -3919,10 +3924,10 @@ async fn join_channel_internal(
|
||||
| Status::Reconnecting
|
||||
| Status::Reauthenticating => continue,
|
||||
Status::Connected { .. } => break 'outer,
|
||||
Status::SignedOut => return Err(anyhow!("not signed in")),
|
||||
Status::UpgradeRequired => return Err(anyhow!("zed is out of date")),
|
||||
Status::SignedOut => return Err(ErrorCode::SignedOut.into()),
|
||||
Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()),
|
||||
Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
|
||||
return Err(anyhow!("zed is offline"))
|
||||
return Err(ErrorCode::Disconnected.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3995,9 +4000,27 @@ pub fn join_channel(
|
||||
if let Some(active_window) = active_window {
|
||||
active_window
|
||||
.update(&mut cx, |_, cx| {
|
||||
let detail: SharedString = match err.error_code() {
|
||||
ErrorCode::SignedOut => {
|
||||
"Please sign in to continue.".into()
|
||||
},
|
||||
ErrorCode::UpgradeRequired => {
|
||||
"Your are running an unsupported version of Zed. Please update to continue.".into()
|
||||
},
|
||||
ErrorCode::NoSuchChannel => {
|
||||
"No matching channel was found. Please check the link and try again.".into()
|
||||
},
|
||||
ErrorCode::Forbidden => {
|
||||
"This channel is private, and you do not have access. Please ask someone to add you and try again.".into()
|
||||
},
|
||||
ErrorCode::Disconnected => "Please check your internet connection and try again.".into(),
|
||||
ErrorCode::WrongReleaseChannel => format!("Others in the channel are using the {} release of Zed. Please switch to join this call.", err.error_tag("required").unwrap_or("other")).into(),
|
||||
_ => format!("{}\n\nPlease try again.", err).into(),
|
||||
};
|
||||
cx.prompt(
|
||||
PromptLevel::Critical,
|
||||
&format!("Failed to join channel: {}", err),
|
||||
"Failed to join channel",
|
||||
Some(&detail),
|
||||
&["Ok"],
|
||||
)
|
||||
})?
|
||||
@@ -4224,6 +4247,7 @@ pub fn restart(_: &Restart, cx: &mut AppContext) {
|
||||
cx.prompt(
|
||||
PromptLevel::Info,
|
||||
"Are you sure you want to restart?",
|
||||
None,
|
||||
&["Restart", "Cancel"],
|
||||
)
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.120.0"
|
||||
version = "0.120.6"
|
||||
publish = false
|
||||
license = "GPL-3.0-only"
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
dev
|
||||
stable
|
||||
@@ -385,16 +385,12 @@ fn initialize_pane(workspace: &mut Workspace, pane: &View<Pane>, cx: &mut ViewCo
|
||||
}
|
||||
|
||||
fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
|
||||
use std::fmt::Write as _;
|
||||
|
||||
let app_name = cx.global::<ReleaseChannel>().display_name();
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
let mut message = format!("{app_name} {version}");
|
||||
if let Some(sha) = cx.try_global::<AppCommitSha>() {
|
||||
write!(&mut message, "\n\n{}", sha.0).unwrap();
|
||||
}
|
||||
let message = format!("{app_name} {version}");
|
||||
let detail = cx.try_global::<AppCommitSha>().map(|sha| sha.0.as_ref());
|
||||
|
||||
let prompt = cx.prompt(PromptLevel::Info, &message, &["OK"]);
|
||||
let prompt = cx.prompt(PromptLevel::Info, &message, detail, &["OK"]);
|
||||
cx.foreground_executor()
|
||||
.spawn(async {
|
||||
prompt.await.ok();
|
||||
@@ -425,6 +421,7 @@ fn quit(_: &Quit, cx: &mut AppContext) {
|
||||
cx.prompt(
|
||||
PromptLevel::Info,
|
||||
"Are you sure you want to quit?",
|
||||
None,
|
||||
&["Quit", "Cancel"],
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user