fix release and add basic messaging for peers
This commit is contained in:
@@ -3,7 +3,7 @@ name: Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.*.*'
|
||||
- 'v[0-9]*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -20,18 +20,29 @@ jobs:
|
||||
target: x86_64-unknown-linux-gnu
|
||||
artifact: zodia
|
||||
|
||||
- os: macos-14
|
||||
target: aarch64-apple-darwin
|
||||
artifact: zodia
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install system dependencies
|
||||
- name: Install system dependencies (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
libgtk-4-dev \
|
||||
libadwaita-1-dev \
|
||||
libopus-dev \
|
||||
libasound2-dev \
|
||||
pkg-config
|
||||
|
||||
- name: Install system dependencies (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
brew install gtk4 libadwaita opus pkg-config
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
@@ -50,14 +61,94 @@ jobs:
|
||||
- name: Build release binary
|
||||
run: cargo build --release --bin zodia --target ${{ matrix.target }}
|
||||
|
||||
- name: Prepare artifact
|
||||
- name: Prepare Linux artifact
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
BIN=target/${{ matrix.target }}/release/${{ matrix.artifact }}
|
||||
strip "$BIN"
|
||||
cp "$BIN" zodia-${{ matrix.target }}
|
||||
|
||||
- name: Upload to GitHub Release
|
||||
- name: Create macOS .app bundle
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
APP=Zodia.app
|
||||
mkdir -p "$APP/Contents/MacOS"
|
||||
mkdir -p "$APP/Contents/Resources"
|
||||
|
||||
cp target/${{ matrix.target }}/release/${{ matrix.artifact }} \
|
||||
"$APP/Contents/MacOS/zodia-bin"
|
||||
|
||||
cat > "$APP/Contents/Info.plist" <<'PLIST'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key>
|
||||
<string>Zodia</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Zodia</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>app.zodia.Zodia</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${{ github.ref_name }}</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>${{ github.ref_name }}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>zodia</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>13.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>AGPL-3.0-or-later</string>
|
||||
</dict>
|
||||
</plist>
|
||||
PLIST
|
||||
|
||||
# Bundle shared libraries so the .app is self-contained
|
||||
brew install dylibbundler
|
||||
dylibbundler \
|
||||
--bundle-deps \
|
||||
--fix-file "$APP/Contents/MacOS/zodia-bin" \
|
||||
--dest-dir "$APP/Contents/libs" \
|
||||
--install-path "@executable_path/../libs" \
|
||||
--overwrite-dir
|
||||
|
||||
# Copy Adwaita icon theme so symbolic icons render
|
||||
ICONS_SRC="$(brew --prefix)/share/icons"
|
||||
if [ -d "$ICONS_SRC/hicolor" ]; then
|
||||
mkdir -p "$APP/Contents/Resources/share/icons"
|
||||
cp -r "$ICONS_SRC/hicolor" "$APP/Contents/Resources/share/icons/"
|
||||
fi
|
||||
if [ -d "$ICONS_SRC/Adwaita" ]; then
|
||||
mkdir -p "$APP/Contents/Resources/share/icons"
|
||||
cp -r "$ICONS_SRC/Adwaita" "$APP/Contents/Resources/share/icons/"
|
||||
fi
|
||||
|
||||
# Launcher wrapper — sets XDG_DATA_DIRS so GTK finds our bundled icons
|
||||
cat > "$APP/Contents/MacOS/zodia" <<'SH'
|
||||
#!/bin/sh
|
||||
BUNDLE="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
export XDG_DATA_DIRS="$BUNDLE/Contents/Resources/share:${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
|
||||
exec "$BUNDLE/Contents/MacOS/zodia-bin" "$@"
|
||||
SH
|
||||
chmod +x "$APP/Contents/MacOS/zodia"
|
||||
|
||||
zip -r "zodia-${{ matrix.target }}.zip" "$APP"
|
||||
|
||||
- name: Upload Linux artifact to GitHub Release
|
||||
if: runner.os == 'Linux'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: zodia-${{ matrix.target }}
|
||||
generate_release_notes: true
|
||||
|
||||
- name: Upload macOS artifact to GitHub Release
|
||||
if: runner.os == 'macOS'
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: zodia-${{ matrix.target }}.zip
|
||||
generate_release_notes: false
|
||||
|
||||
+42
-2
@@ -30,7 +30,7 @@ use zodia_store::{StoreError, ZodiaStore};
|
||||
use crate::aspect_list;
|
||||
use crate::aspect_view::AspectView;
|
||||
use crate::peer_list::DiscoveredPeer;
|
||||
use crate::peer_page;
|
||||
use crate::peer_page::{self, append_chat_row};
|
||||
use crate::util::{approximate_aspects, sign_glyph};
|
||||
|
||||
// ── init ──────────────────────────────────────────────────────────────────────
|
||||
@@ -79,6 +79,8 @@ pub enum AppMsg {
|
||||
AcceptCall,
|
||||
RejectCall,
|
||||
HangUp,
|
||||
/// User sent a chat message to a connected peer.
|
||||
SendChat { peer_id: PeerId, text: String },
|
||||
}
|
||||
|
||||
// ── model ─────────────────────────────────────────────────────────────────────
|
||||
@@ -114,6 +116,9 @@ pub struct AppModel {
|
||||
|
||||
call_state: CallState,
|
||||
active_audio: Option<AudioSession>,
|
||||
|
||||
/// Chat history per peer: `(from_us, text)`.
|
||||
chat_logs: HashMap<PeerId, Vec<(bool, String)>>,
|
||||
}
|
||||
|
||||
// ── widgets ───────────────────────────────────────────────────────────────────
|
||||
@@ -134,6 +139,11 @@ pub struct AppWidgets {
|
||||
/// Generation of the peer list we last rendered.
|
||||
peer_list_shown_gen: u64,
|
||||
|
||||
/// Message list widget per peer (keyed by 4-byte hex tag).
|
||||
peer_msg_lists: HashMap<String, gtk::ListBox>,
|
||||
/// How many messages from `chat_logs` have already been appended to each list.
|
||||
peer_chat_shown: HashMap<String, usize>,
|
||||
|
||||
peers_page: adw::ViewStackPage,
|
||||
node_id_label: gtk::Label,
|
||||
|
||||
@@ -182,6 +192,7 @@ impl AsyncComponent for AppModel {
|
||||
identity,
|
||||
call_state: CallState::Idle,
|
||||
active_audio: None,
|
||||
chat_logs: HashMap::new(),
|
||||
};
|
||||
|
||||
if let Some(birth) = model.config.birth.clone() {
|
||||
@@ -345,6 +356,14 @@ impl AsyncComponent for AppModel {
|
||||
self.call_state = CallState::Idle;
|
||||
info!("hung up");
|
||||
}
|
||||
|
||||
AppMsg::SendChat { peer_id, text } => {
|
||||
if let Some(channel) = self.connected_channels.get(&peer_id) {
|
||||
if channel.send_msg(&ChannelMsg::ChatMsg { text: text.clone() }).await.is_ok() {
|
||||
self.chat_logs.entry(peer_id).or_default().push((true, text));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,6 +421,9 @@ impl AsyncComponent for AppModel {
|
||||
self.active_audio = None;
|
||||
self.call_state = CallState::Idle;
|
||||
}
|
||||
ZodiaNetEvent::ChatReceived { from, text } => {
|
||||
self.chat_logs.entry(from).or_default().push((false, text));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -456,7 +478,7 @@ impl AsyncComponent for AppModel {
|
||||
// Only push if not already on the navigation stack.
|
||||
if widgets.peers_nav.find_page(&tag).is_none() {
|
||||
if let Some(chart) = &self.chart {
|
||||
let page = peer_page::build_peer_page(
|
||||
let (page, msg_list) = peer_page::build_peer_page(
|
||||
&peer_id, their_blob, chart,
|
||||
Rc::clone(&self.store),
|
||||
Rc::clone(&self.identity),
|
||||
@@ -464,6 +486,7 @@ impl AsyncComponent for AppModel {
|
||||
);
|
||||
page.set_tag(Some(&tag));
|
||||
widgets.peers_nav.push(&page);
|
||||
widgets.peer_msg_lists.insert(tag, msg_list);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -472,6 +495,21 @@ impl AsyncComponent for AppModel {
|
||||
}
|
||||
}
|
||||
|
||||
// ── append new chat messages to peer message lists ────────────────────
|
||||
|
||||
for (peer_id, messages) in &self.chat_logs {
|
||||
let tag = hex::encode_upper(&peer_id.0[..4]);
|
||||
let shown = widgets.peer_chat_shown.get(&tag).copied().unwrap_or(0);
|
||||
if messages.len() > shown {
|
||||
if let Some(list) = widgets.peer_msg_lists.get(&tag) {
|
||||
for (from_us, text) in &messages[shown..] {
|
||||
append_chat_row(list, text, *from_us);
|
||||
}
|
||||
widgets.peer_chat_shown.insert(tag, messages.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── node ID label ─────────────────────────────────────────────────────
|
||||
|
||||
if !self.node_id_text.is_empty() {
|
||||
@@ -791,6 +829,8 @@ fn build_widgets(
|
||||
peers_nav,
|
||||
peers_content,
|
||||
peer_list_shown_gen: u64::MAX, // force initial build
|
||||
peer_msg_lists: HashMap::new(),
|
||||
peer_chat_shown: HashMap::new(),
|
||||
peers_page,
|
||||
node_id_label,
|
||||
call_bar,
|
||||
|
||||
+114
-8
@@ -1,11 +1,13 @@
|
||||
//! Connected-peer navigation page.
|
||||
//!
|
||||
//! Pushed onto the app's `adw::NavigationView` when a Tier-1 exchange
|
||||
//! completes. Shows two tabs:
|
||||
//! Pushed onto the Peers tab's `adw::NavigationView` when a Tier-1 exchange
|
||||
//! completes. Shows three tabs:
|
||||
//! - **Their Chart** — peer's planet placements + their natal aspects.
|
||||
//! - **Synastry** — cross-chart aspects between the two of you.
|
||||
//! - **Messages** — live text chat over the Tier-1 QUIC channel.
|
||||
//!
|
||||
//! A call button lives in the shared HeaderBar.
|
||||
//! Returns `(NavigationPage, gtk::ListBox)` — the caller appends chat rows
|
||||
//! to the `ListBox` whenever new messages arrive.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
@@ -26,7 +28,8 @@ use crate::util::sign_glyph;
|
||||
|
||||
/// Build the `adw::NavigationPage` for a connected peer.
|
||||
///
|
||||
/// Contains two `AspectView` tabs: their natal chart and your synastry.
|
||||
/// Returns `(page, msg_list)` where `msg_list` is the `gtk::ListBox`
|
||||
/// the app uses to append incoming/outgoing chat rows.
|
||||
#[allow(deprecated)] // ViewSwitcherTitle deprecated in ADW 1.4
|
||||
pub fn build_peer_page(
|
||||
peer_id: &PeerId,
|
||||
@@ -35,7 +38,7 @@ pub fn build_peer_page(
|
||||
store: Rc<RefCell<ZodiaStore>>,
|
||||
identity: Rc<IdentityKeypair>,
|
||||
sender: &AsyncComponentSender<AppModel>,
|
||||
) -> adw::NavigationPage {
|
||||
) -> (adw::NavigationPage, gtk::ListBox) {
|
||||
let peer_hex = hex::encode_upper(&peer_id.0[..4]);
|
||||
|
||||
// ── compute their chart + synastry ────────────────────────────────────────
|
||||
@@ -73,9 +76,15 @@ pub fn build_peer_page(
|
||||
let syn_av = AspectView::new(synastry_items(&synastry), Rc::clone(&store), Rc::clone(&identity));
|
||||
syn_av.widget().set_vexpand(true);
|
||||
let syn_page = view_stack.add_titled(syn_av.widget(), Some("synastry"), "Synastry");
|
||||
syn_page.set_icon_name(Some("people-meet-symbolic"));
|
||||
syn_page.set_icon_name(Some("system-users-symbolic"));
|
||||
|
||||
let _ = (their_page, syn_page);
|
||||
// Messages tab
|
||||
let (messages_widget, msg_list) = build_messages_tab(peer_id, sender);
|
||||
messages_widget.set_vexpand(true);
|
||||
let msg_page = view_stack.add_titled(&messages_widget, Some("messages"), "Messages");
|
||||
msg_page.set_icon_name(Some("mail-unread-symbolic"));
|
||||
|
||||
let _ = (their_page, syn_page, msg_page);
|
||||
|
||||
// ── toolbar view ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -116,5 +125,102 @@ pub fn build_peer_page(
|
||||
.build();
|
||||
toolbar_view.add_bottom_bar(&switcher_bar);
|
||||
|
||||
adw::NavigationPage::new(&toolbar_view, &format!("···{peer_hex}"))
|
||||
let page = adw::NavigationPage::new(&toolbar_view, &format!("···{peer_hex}"));
|
||||
(page, msg_list)
|
||||
}
|
||||
|
||||
// ── messages tab ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// Build the Messages tab content.
|
||||
///
|
||||
/// Returns `(container_widget, msg_list)` — the `msg_list` is the `gtk::ListBox`
|
||||
/// the app layer appends rows to when new messages arrive.
|
||||
fn build_messages_tab(
|
||||
peer_id: &PeerId,
|
||||
sender: &AsyncComponentSender<AppModel>,
|
||||
) -> (gtk::Box, gtk::ListBox) {
|
||||
let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||
|
||||
// Scrollable message list
|
||||
let msg_list = gtk::ListBox::new();
|
||||
msg_list.set_selection_mode(gtk::SelectionMode::None);
|
||||
msg_list.add_css_class("boxed-list");
|
||||
msg_list.set_vexpand(true);
|
||||
|
||||
let scrolled = gtk::ScrolledWindow::new();
|
||||
scrolled.set_vexpand(true);
|
||||
scrolled.set_policy(gtk::PolicyType::Never, gtk::PolicyType::Automatic);
|
||||
scrolled.set_child(Some(&msg_list));
|
||||
vbox.append(&scrolled);
|
||||
|
||||
// Input row at the bottom
|
||||
let input_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
input_row.set_margin_start(12);
|
||||
input_row.set_margin_end(12);
|
||||
input_row.set_margin_top(8);
|
||||
input_row.set_margin_bottom(12);
|
||||
|
||||
let entry = gtk::Entry::new();
|
||||
entry.set_hexpand(true);
|
||||
entry.set_placeholder_text(Some("Message…"));
|
||||
input_row.append(&entry);
|
||||
|
||||
let send_btn = gtk::Button::from_icon_name("mail-send-symbolic");
|
||||
send_btn.add_css_class("suggested-action");
|
||||
send_btn.add_css_class("circular");
|
||||
send_btn.set_tooltip_text(Some("Send"));
|
||||
input_row.append(&send_btn);
|
||||
|
||||
vbox.append(&input_row);
|
||||
|
||||
// Wire send button + Enter key
|
||||
let pid = peer_id.clone();
|
||||
let s = sender.clone();
|
||||
let entry_c = entry.clone();
|
||||
let send = move || {
|
||||
let text = entry_c.text().trim().to_string();
|
||||
if !text.is_empty() {
|
||||
s.input(AppMsg::SendChat { peer_id: pid.clone(), text });
|
||||
entry_c.set_text("");
|
||||
}
|
||||
};
|
||||
|
||||
let send_c = send.clone();
|
||||
send_btn.connect_clicked(move |_| send_c());
|
||||
entry.connect_activate(move |_| send());
|
||||
|
||||
(vbox, msg_list)
|
||||
}
|
||||
|
||||
/// Append a single chat row to a message list.
|
||||
///
|
||||
/// `from_us = true` → right-aligned "you" bubble style.
|
||||
/// `from_us = false` → left-aligned "them" bubble style.
|
||||
pub fn append_chat_row(list: >k::ListBox, text: &str, from_us: bool) {
|
||||
let row = gtk::ListBoxRow::new();
|
||||
row.set_selectable(false);
|
||||
row.set_activatable(false);
|
||||
|
||||
let label = gtk::Label::new(Some(text));
|
||||
label.set_wrap(true);
|
||||
label.set_xalign(if from_us { 1.0 } else { 0.0 });
|
||||
label.set_margin_start(12);
|
||||
label.set_margin_end(12);
|
||||
label.set_margin_top(6);
|
||||
label.set_margin_bottom(6);
|
||||
|
||||
if from_us {
|
||||
label.add_css_class("caption");
|
||||
}
|
||||
|
||||
row.set_child(Some(&label));
|
||||
list.append(&row);
|
||||
|
||||
// Scroll to bottom after appending
|
||||
if let Some(adj) = list.parent()
|
||||
.and_then(|p| p.downcast::<gtk::ScrolledWindow>().ok())
|
||||
.map(|sw| sw.vadjustment())
|
||||
{
|
||||
adj.set_value(adj.upper() - adj.page_size());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,14 @@ use zodia_store::ZodiaStore;
|
||||
// ── zodiac signs ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// Zodiac sign glyph from a solar month index (0 = Aries … 11 = Pisces).
|
||||
///
|
||||
/// Each glyph is followed by U+FE0E (variation selector-15) to force text
|
||||
/// presentation on platforms (macOS) that would otherwise render them as emoji.
|
||||
pub fn sign_glyph(solar_month: u8) -> &'static str {
|
||||
const SIGNS: [&str; 12] = [
|
||||
"♈", "♉", "♊", "♋", "♌", "♍", "♎", "♏", "♐", "♑", "♒", "♓",
|
||||
"♈\u{FE0E}", "♉\u{FE0E}", "♊\u{FE0E}", "♋\u{FE0E}",
|
||||
"♌\u{FE0E}", "♍\u{FE0E}", "♎\u{FE0E}", "♏\u{FE0E}",
|
||||
"♐\u{FE0E}", "♑\u{FE0E}", "♒\u{FE0E}", "♓\u{FE0E}",
|
||||
];
|
||||
SIGNS.get(solar_month as usize % 12).copied().unwrap_or("?")
|
||||
}
|
||||
|
||||
@@ -64,6 +64,8 @@ pub enum ChannelMsg {
|
||||
CallReject { session_id: [u8; 32] },
|
||||
/// Either party ends an active session.
|
||||
CallHangup { session_id: [u8; 32] },
|
||||
/// Plain-text chat message from the remote peer.
|
||||
ChatMsg { text: String },
|
||||
}
|
||||
|
||||
// ── channel ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -88,5 +88,7 @@ pub enum ZodiaNetEvent {
|
||||
CallHungUp { from: PeerId },
|
||||
/// An incoming Tier-1 QUIC connection was accepted from a remote peer.
|
||||
IncomingChannel { peer_id: PeerId, channel: DirectChannel },
|
||||
/// A plain-text chat message received from a connected peer.
|
||||
ChatReceived { from: PeerId, text: String },
|
||||
}
|
||||
|
||||
|
||||
@@ -277,7 +277,10 @@ pub(crate) fn spawn_channel_listener(
|
||||
Ok(ChannelMsg::CallHangup { .. }) => {
|
||||
let _ = tx.send(ZodiaNetEvent::CallHungUp { from: peer_id.clone() }).await;
|
||||
}
|
||||
Ok(_) => {} // Tier1Handshake already handled at connect time
|
||||
Ok(ChannelMsg::ChatMsg { text }) => {
|
||||
let _ = tx.send(ZodiaNetEvent::ChatReceived { from: peer_id.clone(), text }).await;
|
||||
}
|
||||
Ok(_) => {} // Tier1Handshake / InterpShare already handled at connect time
|
||||
Err(e) => {
|
||||
debug!(peer = %hex::encode_upper(&peer_id.0[..4]), err = %e, "channel closed");
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user