From 287b3664c2e4487835ce79e1bdca2f3190665327 Mon Sep 17 00:00:00 2001 From: Justin Scroggins Date: Sat, 24 Oct 2026 16:42:04 -0400 Subject: [PATCH 1/2] Hide cached chats while an account is signed out. The window was still listing and opening history after Telegram reported signed out. A release of chatd now refuses to compile without the Telegram app id. Co-authored-by: Cursor --- crates/chatd/build.rs | 17 ++++ crates/chatd/src/server.rs | 118 ++++++++++++++++++++++----- crates/chatd/src/store.rs | 56 ++++++++++--- crates/tinyclient/src/gui/sidebar.rs | 34 +++++++- crates/tinyclient/src/gui/state.rs | 49 +++++++++++ packaging/bundle.sh | 16 ++++ 6 files changed, 258 insertions(+), 32 deletions(-) create mode 100644 crates/chatd/build.rs diff --git a/crates/chatd/build.rs b/crates/chatd/build.rs new file mode 100644 index 0000000..365d55a --- /dev/null +++ b/crates/chatd/build.rs @@ -0,0 +1,17 @@ +fn main() { + println!("cargo:rerun-if-env-changed=TINYCLIENT_TELEGRAM_API_ID"); + println!("cargo:rerun-if-env-changed=TINYCLIENT_TELEGRAM_API_HASH"); + // Debug and test builds may omit the id: the Telegram tests assert the "without an app id" + // message. A release is what gets installed, and it cannot sign in without these. + if std::env::var("PROFILE").as_deref() != Ok("release") { + return; + } + let id = std::env::var("TINYCLIENT_TELEGRAM_API_ID").unwrap_or_default(); + let hash = std::env::var("TINYCLIENT_TELEGRAM_API_HASH").unwrap_or_default(); + let id_ok = id.trim().parse::().ok().is_some_and(|id| id > 0); + if !id_ok || hash.trim().is_empty() { + panic!( + "refusing to build a release of chatd without TINYCLIENT_TELEGRAM_API_ID and TINYCLIENT_TELEGRAM_API_HASH" + ); + } +} diff --git a/crates/chatd/src/server.rs b/crates/chatd/src/server.rs index 1c97b57..716caf3 100644 --- a/crates/chatd/src/server.rs +++ b/crates/chatd/src/server.rs @@ -475,6 +475,39 @@ impl Chat { } } + /// Whether this account's cached chats and messages may be shown. A signed-out account keeps + /// them on disk so the next sign-in has them, and the window is shown none of them. A + /// keychain read still in progress is not a sign-out. Scratch directories have no sessions + /// and keep showing what was seeded. + fn shows(&self, account: &AccountKey) -> bool { + if !session::enabled() { + return true; + } + if self.hub.link(account) != Link::SignedOut { + return true; + } + let network = account.network; + let keychain = self.hub.keychain(); + keychain.denied.contains(&network) + || keychain.waiting == Some(network) + || self.hub.sessions_pending().contains(&network) + } + + /// Accounts whose stored rows must not be listed. + fn withheld(&self) -> Vec { + if !session::enabled() { + return Vec::new(); + } + self.hub + .store() + .accounts() + .unwrap_or_default() + .into_iter() + .map(|row| row.key) + .filter(|key| !self.shows(key)) + .collect() + } + /// A row as the shell is told it: what is stored, and who is in it right now. fn row(&self, mut conversation: Conversation) -> Conversation { if conversation.kind == ConversationKind::Voice || conversation.call == CallSupport::Native { @@ -573,6 +606,15 @@ impl Chat { } fn messages(&self, conversation: Key, page: Page, limit: u32) -> Response { + if !self.shows(&conversation.account()) { + return Response::Messages { + conversation, + messages: Vec::new(), + last_read: None, + older: false, + newer: false, + }; + } let read = { let mut store = self.hub.store(); let failed = store.fail_pending(now() - SEND_SECONDS, "The network did not answer."); @@ -1096,6 +1138,7 @@ impl Chat { fn in_voice(&self, network: Option) -> Response { let mut occupied = self.hub.occupied(); occupied.retain(|(conversation, _)| network.is_none_or(|network| conversation.network == network)); + occupied.retain(|(conversation, _)| self.shows(&conversation.account())); occupied.sort_by_key(|(conversation, occupants)| (std::cmp::Reverse(occupants.total), conversation.id.clone())); let mut rows = Vec::new(); { @@ -1136,7 +1179,7 @@ impl Chat { let mut seen = std::collections::HashSet::new(); let talked_ids: std::collections::HashSet = talked.iter().map(|person| person.key.clone()).collect(); for person in self.with_presence(owned.into_iter().chain(talked).collect()) { - if !seen.insert(person.key.clone()) { + if !self.shows(&person.key.account()) || !seen.insert(person.key.clone()) { continue; } if person.activity.as_ref().is_some_and(|activity| activity.kind.in_call()) { @@ -1157,6 +1200,9 @@ impl Chat { /// What was shared: from the cache, saying it is only that; or, because the user asked, /// from the network's own search. fn shared(&self, conversation: Key, kind: SharedKind, before: Option, limit: u32, search: bool) -> Response { + if !self.shows(&conversation.account()) { + return error(ErrorCode::NotFound, "signed out"); + } let limit = limit.min(PAGE_LIMIT); if !search { return match self.hub.store().shared(&conversation, kind, before.as_deref(), limit) { @@ -1334,10 +1380,11 @@ impl Chat { } Request::Inbox { network, filter, offset, limit } => { + let hidden = self.withheld(); let read = { let store = self.hub.store(); - let page = store.inbox(network, filter, offset, limit.min(PAGE_LIMIT)); - page.and_then(|(conversations, total)| Ok((conversations, total, store.inbox_counts(network)?))) + let page = store.inbox(network, filter, offset, limit.min(PAGE_LIMIT), &hidden); + page.and_then(|(conversations, total)| Ok((conversations, total, store.inbox_counts(network, &hidden)?))) }; match read { Ok((conversations, total, counts)) => Response::Conversations { @@ -1349,7 +1396,8 @@ impl Chat { } } Request::Home { network, account, folder, shelf, offset, limit } => { - let read = self.hub.store().home(network, account.as_ref(), folder.as_deref(), shelf, offset, limit.min(PAGE_LIMIT)); + let hidden = self.withheld(); + let read = self.hub.store().home(network, account.as_ref(), folder.as_deref(), shelf, offset, limit.min(PAGE_LIMIT), &hidden); if offset == 0 { for linked in self.hub.links() { let mine = linked.account.network == network && account.as_ref().is_none_or(|account| *account == linked.account); @@ -1368,6 +1416,9 @@ impl Chat { Err(err) => internal(err), }, Request::Threads { space, offset, limit } => { + if !self.shows(&space.account()) { + return Response::Conversations { conversations: Vec::new(), total: 0, counts: InboxCounts::default() }; + } let read = self.hub.store().threads(&space, offset, limit.min(PAGE_LIMIT)); if offset == 0 { self.command(&space.account(), Command::OpenThreads(space.id.clone())); @@ -1379,12 +1430,17 @@ impl Chat { Err(err) => internal(err), } } - Request::Topics { forum, offset, limit } => match self.hub.store().topics(&forum, offset, limit.min(PAGE_LIMIT)) { - Ok((conversations, total)) => { - Response::Conversations { conversations: self.rows(conversations), total, counts: InboxCounts::default() } + Request::Topics { forum, offset, limit } => { + if !self.shows(&forum.account()) { + return Response::Conversations { conversations: Vec::new(), total: 0, counts: InboxCounts::default() }; } - Err(err) => internal(err), - }, + match self.hub.store().topics(&forum, offset, limit.min(PAGE_LIMIT)) { + Ok((conversations, total)) => { + Response::Conversations { conversations: self.rows(conversations), total, counts: InboxCounts::default() } + } + Err(err) => internal(err), + } + } Request::InVoice { network } => self.in_voice(network), Request::Around { network } => self.around(network), Request::Catalog { account, kind, space, query, cursor } => self.catalog(account, kind, space, query, cursor), @@ -1408,11 +1464,14 @@ impl Chat { false => self.hub.store().find(network, query, limit.min(PAGE_LIMIT)), }; match found { - Ok(conversations) => Response::Conversations { - total: conversations.len() as u32, - conversations: self.rows(conversations), - counts: InboxCounts::default(), - }, + Ok(conversations) => { + let conversations: Vec<_> = conversations.into_iter().filter(|row| self.shows(&row.key.account())).collect(); + Response::Conversations { + total: conversations.len() as u32, + conversations: self.rows(conversations), + counts: InboxCounts::default(), + } + } Err(err) => internal(err), } } @@ -1423,7 +1482,10 @@ impl Chat { false => self.hub.store().search_messages(conversation.as_ref(), network, space.as_ref(), query, limit.min(PAGE_LIMIT)), }; match found { - Ok(hits) => Response::MessageHits { hits }, + Ok(hits) => { + let hits: Vec<_> = hits.into_iter().filter(|hit| self.shows(&hit.conversation.account())).collect(); + Response::MessageHits { hits } + } Err(err) => internal(err), } } @@ -1439,6 +1501,9 @@ impl Chat { return error(ErrorCode::Unsupported, "a search on the network names a conversation or a Space") } }; + if !self.shows(&account) { + return Response::MessageHits { hits: Vec::new() }; + } match self.ask(&account, |reply| Command::Search { query, scope, at: std::time::Instant::now(), reply }) { Ok(hits) => Response::MessageHits { hits }, Err(refused) => *refused, @@ -1451,10 +1516,16 @@ impl Chat { Err(err) => internal(err), }, Request::Spaces { network } => match self.hub.store().spaces(network) { - Ok(spaces) => Response::Spaces { spaces }, + Ok(spaces) => { + let spaces = spaces.into_iter().filter(|space| self.shows(&space.key.account())).collect(); + Response::Spaces { spaces } + } Err(err) => internal(err), }, Request::Tree { space, offset, limit, peek } => { + if !self.shows(&space.account()) { + return error(ErrorCode::NotFound, "signed out"); + } let tree = self.hub.store().tree(&space, offset, limit.min(TREE_LIMIT)); match tree { Ok(Some(tree)) => { @@ -1472,11 +1543,16 @@ impl Chat { Err(err) => internal(err), } } - Request::Conversation { conversation } => match self.hub.store().conversation(&conversation) { - Ok(Some(conversation)) => Response::Conversation { conversation: self.row(conversation) }, - Ok(None) => error(ErrorCode::NotFound, "no such conversation"), - Err(err) => internal(err), - }, + Request::Conversation { conversation } => { + if !self.shows(&conversation.account()) { + return error(ErrorCode::NotFound, "signed out"); + } + match self.hub.store().conversation(&conversation) { + Ok(Some(conversation)) => Response::Conversation { conversation: self.row(conversation) }, + Ok(None) => error(ErrorCode::NotFound, "no such conversation"), + Err(err) => internal(err), + } + } Request::Collapse { space, group, collapsed } => { match self.hub.store().set_collapsed(&space, &group, collapsed) { Ok(true) => Response::Ok, diff --git a/crates/chatd/src/store.rs b/crates/chatd/src/store.rs index e5efcff..03428cf 100644 --- a/crates/chatd/src/store.rs +++ b/crates/chatd/src/store.rs @@ -465,6 +465,24 @@ pub struct OnDisk { pub bytes: u64, } +/// SQL that leaves these accounts out of a conversation query. An id has to be the same shape a +/// session account is allowed to have; anything else is skipped rather than written into SQL. +fn not_these(hidden: &[AccountKey]) -> String { + let parts: Vec = hidden + .iter() + .filter(|account| { + let id = account.account.as_str(); + !id.is_empty() && id.chars().all(|c| c.is_ascii_alphanumeric() || "._-".contains(c)) + }) + .map(|account| format!("(c.network = '{}' AND c.account = '{}')", word(&account.network), account.account)) + .collect(); + if parts.is_empty() { + String::new() + } else { + format!(" AND NOT ({})", parts.join(" OR ")) + } +} + /// How an enum is spelled on the wire, which is also how it is stored. fn word(value: &T) -> String { match serde_json::to_value(value) { @@ -1230,6 +1248,7 @@ impl Store { filter: InboxFilter, offset: u32, limit: u32, + hidden: &[AccountKey], ) -> rusqlite::Result<(Vec, u32)> { let narrowed = match filter { InboxFilter::All => "1", @@ -1237,7 +1256,7 @@ impl Store { InboxFilter::Mentions => IS_MENTION, InboxFilter::Directs => "c.kind IN ('direct', 'group')", }; - let belongs = format!("{IN_INBOX} AND (?1 IS NULL OR c.network = ?1) AND {narrowed}"); + let belongs = format!("{IN_INBOX} AND (?1 IS NULL OR c.network = ?1) AND {narrowed}{}", not_these(hidden)); let first = match filter { InboxFilter::Unread | InboxFilter::Mentions => "c.mentions > 0", InboxFilter::All | InboxFilter::Directs => "c.starred", @@ -1261,12 +1280,13 @@ impl Store { } /// How many rows each Inbox filter holds, in one pass. - pub fn inbox_counts(&self, network: Option) -> rusqlite::Result { + pub fn inbox_counts(&self, network: Option, hidden: &[AccountKey]) -> rusqlite::Result { self.db.query_row( &format!( "SELECT COUNT(*), COALESCE(SUM({IS_UNREAD}), 0), COALESCE(SUM({IS_MENTION}), 0), COALESCE(SUM({IS_APP}), 0) - FROM conversation c WHERE {IN_INBOX} AND (?1 IS NULL OR c.network = ?1)" + FROM conversation c WHERE {IN_INBOX} AND (?1 IS NULL OR c.network = ?1){}", + not_these(hidden) ), params![network.map(|network| word(&network))], counts_from, @@ -1284,6 +1304,7 @@ impl Store { shelf: Shelf, offset: u32, limit: u32, + hidden: &[AccountKey], ) -> rusqlite::Result<(Vec, u32, InboxCounts)> { let shelf = match shelf { Shelf::Main => "NOT c.archived AND c.special != 'request'", @@ -1294,7 +1315,8 @@ impl Store { "c.network = ?1 AND (?2 IS NULL OR c.account = ?2) AND c.space = '' AND c.parent = '' AND NOT c.hidden AND CASE WHEN ?3 IS NULL THEN {shelf} - ELSE instr(c.folders, ',' || ?3 || ',') > 0 AND c.special != 'request' END" + ELSE instr(c.folders, ',' || ?3 || ',') > 0 AND c.special != 'request' END{}", + not_these(hidden) ); let in_folder_pins = "(SELECT instr(f.pinned, ',' || c.id || ',') FROM folder f WHERE f.network = c.network AND f.account = c.account AND f.id = ?3)"; @@ -2723,7 +2745,7 @@ mod tests { store.ingest(&key, &mention, true).unwrap(); let titles = |store: &Store, filter| -> Vec { - let (rows, total) = store.inbox(None, filter, 0, 10).unwrap(); + let (rows, total) = store.inbox(None, filter, 0, 10, &[]).unwrap(); assert_eq!(rows.len() as u32, total); rows.into_iter().map(|row| row.title).collect() }; @@ -2739,9 +2761,23 @@ mod tests { store.set_muted(&Key::new(&account, "dm"), true).unwrap(); assert_eq!(titles(&store, InboxFilter::Unread), ["#general", "#quiet"]); - let counts = store.inbox_counts(None).unwrap(); + let counts = store.inbox_counts(None, &[]).unwrap(); assert_eq!((counts.all, counts.unread, counts.mentions), (3, 2, 1)); - assert_eq!(store.inbox(Some(Network::Slack), InboxFilter::All, 0, 10).unwrap().1, 0); + assert_eq!(store.inbox(Some(Network::Slack), InboxFilter::All, 0, 10, &[]).unwrap().1, 0); + } + + #[test] + fn the_inbox_leaves_out_an_account_that_is_signed_out() { + let (mut store, key) = store(); + let other = AccountKey { network: Network::Telegram, account: "123".into() }; + store.upsert_conversation(&other, &ConversationRow::new("elyse", ConversationKind::Direct, "Elyse")).unwrap(); + store.ingest(&Key::new(&other, "elyse"), &said("1", "hi", 10), true).unwrap(); + // The seeded channel has no message, so the only inbox row is the signed-out chat. + assert_eq!(store.inbox(None, InboxFilter::All, 0, 10, &[]).unwrap().0.len(), 1); + let (rows, total) = store.inbox(None, InboxFilter::All, 0, 10, &[other.clone()]).unwrap(); + assert!(rows.is_empty() && total == 0); + assert_eq!(store.inbox_counts(None, &[other]).unwrap().all, 0); + let _ = key; } #[test] @@ -2771,12 +2807,12 @@ mod tests { store.set_starred(&at("starred-old"), true).unwrap(); store.mark_read(&at("old"), "old-1").unwrap(); let titles = |store: &Store, filter| -> Vec { - store.inbox(None, filter, 0, 20).unwrap().0.into_iter().map(|row| row.title).collect() + store.inbox(None, filter, 0, 20, &[]).unwrap().0.into_iter().map(|row| row.title).collect() }; // Old, empty, archived, hidden and request rows are out; a starred old one stays; the // app is in, flagged, for the shell to fold. assert_eq!(titles(&store, InboxFilter::All), ["Starred old", "GitHub", "Recent"]); - let counts = store.inbox_counts(None).unwrap(); + let counts = store.inbox_counts(None, &[]).unwrap(); // The app's unread raises no count but its own dot; the archived one and the request // count for nothing. assert_eq!((counts.all, counts.unread, counts.apps), (3, 2, 1), "{counts:?}"); @@ -2829,7 +2865,7 @@ mod tests { ]) .unwrap(); let ids = |store: &Store, folder: Option<&str>, shelf| -> Vec { - let (rows, total, _) = store.home(Network::Discord, None, folder, shelf, 0, 20).unwrap(); + let (rows, total, _) = store.home(Network::Discord, None, folder, shelf, 0, 20, &[]).unwrap(); assert_eq!(rows.len() as u32, total); rows.into_iter().map(|row| row.key.id).collect() }; diff --git a/crates/tinyclient/src/gui/sidebar.rs b/crates/tinyclient/src/gui/sidebar.rs index 8bd1460..94d89ce 100644 --- a/crates/tinyclient/src/gui/sidebar.rs +++ b/crates/tinyclient/src/gui/sidebar.rs @@ -33,7 +33,7 @@ use gpui::{ }; use history::History; use pages::Pages; -use protocol::{Badge, Conversation, ConversationKind, InboxCounts, InboxFilter, Key, Link, Network, Request, Response, PAGE_LIMIT}; +use protocol::{AccountKey, Badge, Conversation, ConversationKind, InboxCounts, InboxFilter, Key, Link, Network, Request, Response, PAGE_LIMIT}; use std::collections::{HashMap, HashSet}; use std::ops::Range; use std::rc::Rc; @@ -198,6 +198,38 @@ impl State { } } + /// Drops rows whose account is signed out. True when the Inbox changed, and when a home list did. + pub fn without_signed_out(&mut self, keep: &HashSet) -> (bool, bool) { + let visible = |key: &Key| keep.contains(&key.account()); + let kept: Vec<_> = self.inbox.all().iter().filter(|row| visible(&row.key)).cloned().collect(); + let inbox_changed = kept.len() != self.inbox.all().len(); + if inbox_changed { + let epoch = self.inbox.epoch(); + let total = kept.len() as u32; + self.inbox.fill(epoch, kept, total); + self.muted_news.retain(|row| visible(&row.key)); + if let Some(waiting) = self.waiting.as_mut() { + waiting.retain(|row| visible(&row.key)); + } + } + let home = &mut self.home; + let before = home.chats.len() + home.drilled.len() + home.in_voice.len() + home.archive.len(); + home.chats.retain(|row| visible(&row.key)); + home.drilled.retain(|row| visible(&row.key)); + home.in_voice.retain(|row| visible(&row.key)); + home.archive.retain(|row| visible(&row.key)); + let mut home_changed = home.chats.len() + home.drilled.len() + home.in_voice.len() + home.archive.len() != before; + for saved in self.saved_homes.values_mut() { + let before = saved.chats.len() + saved.drilled.len() + saved.in_voice.len() + saved.archive.len(); + saved.chats.retain(|row| visible(&row.key)); + saved.drilled.retain(|row| visible(&row.key)); + saved.in_voice.retain(|row| visible(&row.key)); + saved.archive.retain(|row| visible(&row.key)); + home_changed |= saved.chats.len() + saved.drilled.len() + saved.in_voice.len() + saved.archive.len() != before; + } + (inbox_changed, home_changed) + } + /// Every row the sidebar holds, wherever it is drawn. pub fn held(&self) -> impl Iterator> { let home = &self.home; diff --git a/crates/tinyclient/src/gui/state.rs b/crates/tinyclient/src/gui/state.rs index edd57b7..066208c 100644 --- a/crates/tinyclient/src/gui/state.rs +++ b/crates/tinyclient/src/gui/state.rs @@ -354,9 +354,58 @@ impl Shell { self.sidebar_state.mode = Mode::Inbox; } } + // A signed-out account keeps its cache on disk and shows none of it. + self.drop_signed_out(cx); cx.notify(); } + /// Drops chats, the open thread, and list rows for accounts that are signed out. While the + /// keychain is still being read those accounts have not arrived yet, so nothing is dropped. + fn drop_signed_out(&mut self, cx: &mut Context) { + if self.service.keychain.reading { + return; + } + let Some(snapshot) = &self.snapshot else { + return; + }; + let keep: HashSet = snapshot + .accounts + .iter() + .filter(|account| account.link != Link::SignedOut) + .map(|account| account.key.clone()) + .collect(); + let visible = |key: &Key| keep.contains(&key.account()); + + if self.open.as_ref().is_some_and(|key| !visible(key)) { + self.open = None; + if let Some(snapshot) = &mut self.snapshot { + snapshot.open = None; + } + } + + let (inbox_changed, home_changed) = self.sidebar_state.without_signed_out(&keep); + let trees_changed = { + let mut changed = false; + for tree in self.trees.values_mut() { + let before = tree.conversations.len(); + tree.conversations.retain(|row| visible(&row.key)); + changed |= tree.conversations.len() != before; + } + changed + }; + if inbox_changed { + self.rebuild_inbox(cx); + let pages = self.sidebar_state.inbox.stale(); + self.load_inbox(pages, cx); + } + if home_changed || trees_changed { + self.rebuild_rows(cx); + } + if home_changed { + self.load_home(cx); + } + } + pub(super) fn load_status(&mut self, cx: &mut Context) { self.off_thread(cx, || ask(&Request::Status), |this, answer, cx| { if let Ok(Response::Status { networks, accounts, call, .. }) = answer { diff --git a/packaging/bundle.sh b/packaging/bundle.sh index 1335fe7..739db98 100755 --- a/packaging/bundle.sh +++ b/packaging/bundle.sh @@ -20,6 +20,22 @@ app=$target/$profile/tinyclient.app version=$(sed -n 's/^version = "\(.*\)"/\1/p' "$root/Cargo.toml" | head -1) if [ "${1:-}" != "--no-build" ]; then + # chatd embeds the Telegram app id at compile time. A release without it cannot sign in. + # --no-build only packs binaries that were already compiled, which is what CI does after + # the release build step has already refused a chatd with no id. + if [ -z "${TINYCLIENT_TELEGRAM_API_ID:-}" ] || [ -z "${TINYCLIENT_TELEGRAM_API_HASH:-}" ]; then + env_file=${TINYCLIENT_TELEGRAM_ENV:-"$HOME/.config/tinyclient/telegram.env"} + if [ -f "$env_file" ]; then + set -a + # shellcheck disable=SC1090 + . "$env_file" + set +a + fi + fi + if [ -z "${TINYCLIENT_TELEGRAM_API_ID:-}" ] || [ -z "${TINYCLIENT_TELEGRAM_API_HASH:-}" ]; then + echo "bundle.sh: set TINYCLIENT_TELEGRAM_API_ID and TINYCLIENT_TELEGRAM_API_HASH before building" >&2 + exit 1 + fi cargo build --release -j "${JOBS:-6}" --manifest-path "$root/Cargo.toml" --target-dir "$target" \ -p tinyclient --features tinyclient/gui -p chatd -p mediad -p login fi From e3cfe3162a7e21db33094a703cdb70517c23f08a Mon Sep 17 00:00:00 2001 From: Justin Scroggins Date: Sat, 24 Oct 2026 16:42:09 -0400 Subject: [PATCH 2/2] Version 0.2.8: hide signed-out history. A copy that already says 0.2.7 is not offered this fix. This one is newer, so Check for Updates can replace it. Co-authored-by: Cursor --- Cargo.lock | 10 +++++----- Cargo.toml | 2 +- README.md | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e61c189..d54390e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1015,7 +1015,7 @@ dependencies = [ [[package]] name = "chatd" -version = "0.2.7" +version = "0.2.8" dependencies = [ "aes 0.8.4", "aes-gcm", @@ -4934,7 +4934,7 @@ dependencies = [ [[package]] name = "login" -version = "0.2.7" +version = "0.2.8" dependencies = [ "block2 0.6.2", "embed-resource", @@ -5094,7 +5094,7 @@ checksum = "7ebb8d8732c6a6df3d8f032a82911cfc747e00efb95cc46e8d0acd5b5b88570c" [[package]] name = "mediad" -version = "0.2.7" +version = "0.2.8" dependencies = [ "aes-gcm", "chacha20poly1305", @@ -7013,7 +7013,7 @@ dependencies = [ [[package]] name = "protocol" -version = "0.2.7" +version = "0.2.8" dependencies = [ "interprocess", "serde", @@ -9077,7 +9077,7 @@ dependencies = [ [[package]] name = "tinyclient" -version = "0.2.7" +version = "0.2.8" dependencies = [ "embed-resource", "gpui", diff --git a/Cargo.toml b/Cargo.toml index 8723b80..f518a53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ members = [ ] [workspace.package] -version = "0.2.7" +version = "0.2.8" edition = "2021" license = "MIT" publish = false diff --git a/README.md b/README.md index f0c7b66..0c70183 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Sizes of a macOS arm64 release build, 2026-10-14: ## What works today -Current release: [v0.2.7](https://github.com/itz4blitz/tinyclient/releases/tag/v0.2.7). This is what `main` does. The goal is full parity with each official app. Every gap is a place to help. +Current release: [v0.2.8](https://github.com/itz4blitz/tinyclient/releases/tag/v0.2.8). This is what `main` does. The goal is full parity with each official app. Every gap is a place to help. | Network | Sign-in | Reading and media | Writing | Calls | | --- | --- | --- | --- | --- |