4 Commits

Author SHA1 Message Date
a81022bb0b add failed message 2026-03-28 15:25:32 +07:00
d36364d60d chore: update nostr sdk 2026-03-25 13:52:21 +07:00
99363475e0 chore: fix prepare flathub script 2026-03-20 13:22:58 +07:00
a52e1877fe chore: add prepare flathub script (#24)
Reviewed-on: #24
Co-authored-by: Ren Amamiya <reya@lume.nu>
Co-committed-by: Ren Amamiya <reya@lume.nu>
2026-03-20 06:20:06 +00:00
12 changed files with 859 additions and 314 deletions

3
.gitignore vendored
View File

@@ -21,3 +21,6 @@ dist/
.DS_Store .DS_Store
# Added by goreleaser init: # Added by goreleaser init:
.intentionally-empty-file.o .intentionally-empty-file.o
.cargo/
vendor/

637
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
use std::cmp::Reverse; use std::cmp::Reverse;
use std::collections::{HashMap, HashSet}; use std::collections::{BTreeSet, HashMap, HashSet};
use std::hash::{DefaultHasher, Hash, Hasher}; use std::hash::{DefaultHasher, Hash, Hasher};
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
@@ -55,7 +55,24 @@ enum Signal {
/// Eose received from relay pool /// Eose received from relay pool
Eose, Eose,
/// An error occurred /// An error occurred
Error(SharedString), Error(FailedMessage),
}
impl Signal {
pub fn message(gift_wrap: EventId, rumor: UnsignedEvent) -> Self {
Self::Message(NewMessage::new(gift_wrap, rumor))
}
pub fn eose() -> Self {
Self::Eose
}
pub fn error<T>(event: &Event, reason: T) -> Self
where
T: Into<SharedString>,
{
Self::Error(FailedMessage::new(event, reason))
}
} }
/// Chat Registry /// Chat Registry
@@ -64,6 +81,9 @@ pub struct ChatRegistry {
/// Chat rooms /// Chat rooms
rooms: Vec<Entity<Room>>, rooms: Vec<Entity<Room>>,
/// Events that failed to unwrap for any reason
trashes: Entity<BTreeSet<FailedMessage>>,
/// Tracking events seen on which relays in the current session /// Tracking events seen on which relays in the current session
seens: Arc<RwLock<HashMap<EventId, HashSet<RelayUrl>>>>, seens: Arc<RwLock<HashMap<EventId, HashSet<RelayUrl>>>>,
@@ -128,6 +148,7 @@ impl ChatRegistry {
Self { Self {
rooms: vec![], rooms: vec![],
trashes: cx.new(|_| BTreeSet::default()),
seens: Arc::new(RwLock::new(HashMap::default())), seens: Arc::new(RwLock::new(HashMap::default())),
tracking_flag: Arc::new(AtomicBool::new(false)), tracking_flag: Arc::new(AtomicBool::new(false)),
signal_rx: rx, signal_rx: rx,
@@ -144,6 +165,7 @@ impl ChatRegistry {
let signer = nostr.read(cx).signer(); let signer = nostr.read(cx).signer();
let status = self.tracking_flag.clone(); let status = self.tracking_flag.clone();
let seens = self.seens.clone(); let seens = self.seens.clone();
let trashes = self.trashes.downgrade();
let initialized_at = Timestamp::now(); let initialized_at = Timestamp::now();
let sub_id1 = SubscriptionId::new(DEVICE_GIFTWRAP); let sub_id1 = SubscriptionId::new(DEVICE_GIFTWRAP);
@@ -163,7 +185,7 @@ impl ChatRegistry {
continue; continue;
}; };
match message { match *message {
RelayMessage::Event { event, .. } => { RelayMessage::Event { event, .. } => {
// Keep track of which relays have seen this event // Keep track of which relays have seen this event
{ {
@@ -185,28 +207,30 @@ impl ChatRegistry {
match extract_rumor(&client, &signer, event.as_ref()).await { match extract_rumor(&client, &signer, event.as_ref()).await {
Ok(rumor) => { Ok(rumor) => {
if rumor.tags.is_empty() { if rumor.tags.is_empty() {
let error: SharedString = "No room for message".into(); let signal =
tx.send_async(Signal::Error(error)).await?; Signal::error(event.as_ref(), "Recipient is missing");
tx.send_async(signal).await?;
continue;
} }
if rumor.created_at >= initialized_at { if rumor.created_at >= initialized_at {
let new_message = NewMessage::new(event.id, rumor); let signal = Signal::message(event.id, rumor);
let signal = Signal::Message(new_message);
tx.send_async(signal).await?; tx.send_async(signal).await?;
} else { } else {
status.store(true, Ordering::Release); status.store(true, Ordering::Release);
} }
} }
Err(e) => { Err(e) => {
let error: SharedString = format!("Failed to unwrap: {e}").into(); let reason = format!("Failed to extract rumor: {e}");
tx.send_async(Signal::Error(error)).await?; let signal = Signal::error(event.as_ref(), reason);
tx.send_async(signal).await?;
} }
} }
} }
RelayMessage::EndOfStoredEvents(id) => { RelayMessage::EndOfStoredEvents(id) => {
if id.as_ref() == &sub_id1 || id.as_ref() == &sub_id2 { if id.as_ref() == &sub_id1 || id.as_ref() == &sub_id2 {
tx.send_async(Signal::Eose).await?; tx.send_async(Signal::eose()).await?;
} }
} }
_ => {} _ => {}
@@ -229,9 +253,10 @@ impl ChatRegistry {
this.get_rooms(cx); this.get_rooms(cx);
})?; })?;
} }
Signal::Error(error) => { Signal::Error(trash) => {
this.update(cx, |_this, cx| { trashes.update(cx, |this, cx| {
cx.emit(ChatEvent::Error(error)); this.insert(trash);
cx.notify();
})?; })?;
} }
}; };
@@ -685,8 +710,8 @@ async fn extract_rumor(
gift_wrap: &Event, gift_wrap: &Event,
) -> Result<UnsignedEvent, Error> { ) -> Result<UnsignedEvent, Error> {
// Try to get cached rumor first // Try to get cached rumor first
if let Ok(event) = get_rumor(client, gift_wrap.id).await { if let Ok(rumor) = get_rumor(client, gift_wrap.id).await {
return Ok(event); return Ok(rumor);
} }
// Try to unwrap with the available signer // Try to unwrap with the available signer

View File

@@ -2,6 +2,7 @@ use std::hash::Hash;
use std::ops::Range; use std::ops::Range;
use common::{EventUtils, NostrParser}; use common::{EventUtils, NostrParser};
use gpui::SharedString;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
/// New message. /// New message.
@@ -24,6 +25,25 @@ impl NewMessage {
} }
} }
/// Trash message.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct FailedMessage {
pub raw_event: String,
pub reason: SharedString,
}
impl FailedMessage {
pub fn new<T>(event: &Event, reason: T) -> Self
where
T: Into<SharedString>,
{
Self {
raw_event: event.as_json(),
reason: reason.into(),
}
}
}
/// Message. /// Message.
#[derive(Debug, Clone, Hash, PartialEq, Eq)] #[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum Message { pub enum Message {

View File

@@ -201,15 +201,12 @@ impl ChatPanel {
let mut notifications = client.notifications(); let mut notifications = client.notifications();
while let Some(notification) = notifications.next().await { while let Some(notification) = notifications.next().await {
if let ClientNotification::Message { if let ClientNotification::Message { message, relay_url } = notification
message: && let RelayMessage::Ok {
RelayMessage::Ok {
event_id, event_id,
status, status,
message, message,
}, } = *message
relay_url,
} = notification
{ {
let sent_ids = sent_ids.read().await; let sent_ids = sent_ids.read().await;

View File

@@ -44,6 +44,7 @@ fn main() {
cx.set_menus(vec![Menu { cx.set_menus(vec![Menu {
name: "Coop".into(), name: "Coop".into(),
items: vec![MenuItem::action("Quit", Quit)], items: vec![MenuItem::action("Quit", Quit)],
disabled: false,
}]); }]);
// Set up the window bounds // Set up the window bounds

View File

@@ -149,10 +149,8 @@ impl DeviceRegistry {
let mut processed_events = HashSet::new(); let mut processed_events = HashSet::new();
while let Some(notification) = notifications.next().await { while let Some(notification) = notifications.next().await {
if let ClientNotification::Message { if let ClientNotification::Message { message, .. } = notification
message: RelayMessage::Event { event, .. }, && let RelayMessage::Event { event, .. } = *message
..
} = notification
{ {
if !processed_events.insert(event.id) { if !processed_events.insert(event.id) {
// Skip if the event has already been processed // Skip if the event has already been processed

View File

@@ -135,7 +135,7 @@ impl PersonRegistry {
continue; continue;
}; };
if let RelayMessage::Event { event, .. } = message { if let RelayMessage::Event { event, .. } = *message {
// Skip if the event has already been processed // Skip if the event has already been processed
if !processed.insert(event.id) { if !processed.insert(event.id) {
continue; continue;

View File

@@ -100,7 +100,7 @@ impl RelayAuth {
while let Some(notification) = notifications.next().await { while let Some(notification) = notifications.next().await {
if let ClientNotification::Message { relay_url, message } = notification { if let ClientNotification::Message { relay_url, message } = notification {
match message { match *message {
RelayMessage::Auth { challenge } => { RelayMessage::Auth { challenge } => {
if challenges.insert(challenge.clone()) { if challenges.insert(challenge.clone()) {
let request = Arc::new(AuthRequest::new(challenge, relay_url)); let request = Arc::new(AuthRequest::new(challenge, relay_url));
@@ -221,9 +221,8 @@ impl RelayAuth {
while let Some(notification) = notifications.next().await { while let Some(notification) = notifications.next().await {
match notification { match notification {
RelayNotification::Message { RelayNotification::Message { message } => {
message: RelayMessage::Ok { event_id, .. }, if let RelayMessage::Ok { event_id, .. } = *message {
} => {
if id != event_id { if id != event_id {
continue; continue;
} }
@@ -247,6 +246,7 @@ impl RelayAuth {
return Ok(()); return Ok(());
} }
}
RelayNotification::AuthenticationFailed => break, RelayNotification::AuthenticationFailed => break,
_ => {} _ => {}
} }

14
flathub/.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
# Generated files - do not commit to main repo
# These are generated by prepare-flathub.sh
vendor/
vendor.tar.gz
su.reya.coop.yml
su.reya.coop.metainfo.xml
release-info.xml
cargo-config.toml
build/
repo/
# Keep the README and this .gitignore
!README.md
!.gitignore

129
flathub/README.md Normal file
View File

@@ -0,0 +1,129 @@
# Flathub Submission for Coop
This directory contains the files needed to submit Coop to Flathub.
## Prerequisites
- Flatpak installed
- `flatpak-builder` installed
- Rust/Cargo installed (for vendoring)
## Quick Start
Run the preparation script from the repo root:
```bash
./script/prepare-flathub.sh
```
This will:
1. Vendor all Rust dependencies (crates.io + git)
2. Generate the metainfo.xml with proper release info
3. Create `su.reya.coop.yml` - the Flatpak manifest for Flathub
## Files Generated
| File | Purpose |
|------|---------|
| `su.reya.coop.yml` | Main Flatpak manifest (submit this to Flathub) |
| `su.reya.coop.metainfo.xml` | AppStream metadata with release info |
| `vendor.tar.gz` | Vendored Rust dependencies |
| `cargo-config.toml` | Cargo configuration for offline builds |
| `release-info.xml` | Release info snippet for metainfo |
## Testing Locally
Before submitting to Flathub, test the build:
```bash
cd flathub
# Build and install locally
flatpak-builder --user --install --force-clean build su.reya.coop.yml
# Test the app
flatpak run su.reya.coop
# Run the Flathub linter (must pass!)
flatpak run --command=flatpak-builder-lint org.flatpak.Builder manifest su.reya.coop.yml
flatpak run --command=flatpak-builder-lint org.flatpak.Builder repo repo
```
## Submitting to Flathub
### 1. Prepare Your Release
Ensure you have:
- [ ] Committed all changes
- [ ] Tagged the release: `git tag -a v1.0.0-beta2 -m "Release v1.0.0-beta2"`
- [ ] Pushed the tag: `git push origin v1.0.0-beta2`
- [ ] Run `./script/prepare-flathub.sh` to regenerate files
### 2. Fork and Submit
```bash
# Fork https://github.com/flathub/flathub on GitHub first
# Clone your fork (use the new-pr branch!)
git clone --branch=new-pr git@github.com:YOUR_USERNAME/flathub.git
cd flathub
# Create a new branch
git checkout -b su.reya.coop
# Copy ONLY the manifest file from your project
cp /path/to/coop/flathub/su.reya.coop.yml .
# Commit and push
git add su.reya.coop.yml
git commit -m "Add su.reya.coop"
git push origin su.reya.coop
```
### 3. Open Pull Request
1. Go to your fork on GitHub
2. Click "Compare & pull request"
3. **Important:** Set base branch to `new-pr` (not `master`!)
4. Fill in the PR template
5. Submit and wait for review
## What Happens Next?
1. Flathub's automated CI will build your app
2. A maintainer will review your submission
3. Once approved, a new repo `flathub/su.reya.coop` will be created
4. You'll get write access to maintain the app
5. Future updates: Push new commits to `flathub/su.reya.coop`
## Updating the App
To release a new version:
1. Update version in workspace `Cargo.toml`
2. Tag the new release: `git tag -a v1.0.0-beta3 -m "Release v1.0.0-beta3"`
3. Push the tag: `git push origin v1.0.0-beta3`
4. Run `./script/prepare-flathub.sh` to regenerate
5. Clone the flathub repo: `git clone https://github.com/flathub/su.reya.coop.git`
6. Update the manifest with new commit/tag and hashes
7. Submit PR to `flathub/su.reya.coop`
## Troubleshooting
### Build fails with "network access not allowed"
- Make sure `CARGO_NET_OFFLINE=true` is set in the manifest
- Ensure `vendor.tar.gz` is properly extracted before building
### Linter complains about metainfo
- Ensure `su.reya.coop.metainfo.xml` has at least one `<release>` entry
- Check that screenshots are accessible URLs
### Missing dependencies
- If new git dependencies are added, re-run `prepare-flathub.sh`
- The script vendors all dependencies from `Cargo.lock`
## Resources
- [Flathub Submission Docs](https://docs.flathub.org/docs/for-app-authors/submission)
- [Flatpak Manifest Reference](https://docs.flatpak.org/en/latest/manifests.html)
- [AppStream Metainfo Guide](https://www.freedesktop.org/software/appstream/docs/chap-Metadata.html)

245
script/prepare-flathub Executable file
View File

@@ -0,0 +1,245 @@
#!/usr/bin/env bash
#
# Prepare Flathub submission for Coop
# This script:
# 1. Vendors all Rust dependencies (crates.io + git)
# 2. Generates release info for metainfo.xml
# 3. Creates the Flathub manifest (su.reya.coop.yml)
#
# Usage: ./script/prepare-flathub [--release-date YYYY-MM-DD]
set -euo pipefail
cd "$(dirname "$0")/.."
# Configuration
APP_ID="su.reya.coop"
APP_NAME="Coop"
REPO_URL="https://git.reya.su/reya/coop"
BRANDING_LIGHT="#FFE629"
BRANDING_DARK="#FFE629"
# Parse arguments
RELEASE_DATE=""
while [[ $# -gt 0 ]]; do
case $1 in
--release-date)
RELEASE_DATE="$2"
shift 2
;;
-h|--help)
echo "Usage: ${0##*/} [options]"
echo ""
echo "Options:"
echo " --release-date DATE Release date in YYYY-MM-DD format (default: today)"
echo " -h, --help Display this help and exit"
exit 0
;;
*)
echo "Unknown option: $1" >&2
exit 1
;;
esac
done
# Get version from workspace
VERSION=$(script/get-crate-version coop)
if [[ -z "$RELEASE_DATE" ]]; then
RELEASE_DATE=$(date +%Y-%m-%d)
fi
echo "=== Preparing Flathub submission for $APP_NAME v$VERSION ==="
echo ""
# Create flathub directory
mkdir -p flathub
echo "[1/5] Created flathub/ directory"
# Step 2: Vendor all dependencies
echo "[2/5] Vendoring Rust dependencies..."
if [[ -d vendor ]]; then
echo " Removing old vendor directory..."
rm -rf vendor
fi
# Create cargo config for vendoring
mkdir -p .cargo
cat > .cargo/config.toml << 'EOF'
[source.crates-io]
replace-with = "vendored"
[source.vendored]
directory = "vendor"
EOF
# Vendor all dependencies (crates.io + git)
cargo vendor --locked vendor/
echo " Vendored dependencies to vendor/"
# Create tarball of vendored deps
tar -czf flathub/vendor.tar.gz vendor/
echo " Created flathub/vendor.tar.gz"
# Step 3: Generate release info for metainfo
echo "[3/5] Generating release info..."
cat > flathub/release-info.xml << EOF
<release version="${VERSION}" date="${RELEASE_DATE}">
<description>
<p>Release version ${VERSION}</p>
</description>
</release>
EOF
echo " Created flathub/release-info.xml"
# Step 4: Generate the metainfo file with release info
echo "[4/5] Generating metainfo.xml..."
export APP_ID APP_NAME BRANDING_LIGHT BRANDING_DARK
cat crates/coop/resources/flatpak/coop.metainfo.xml.in | \
sed -e "/@release_info@/r flathub/release-info.xml" -e '/@release_info@/d' \
> flathub/${APP_ID}.metainfo.xml
echo " Created flathub/${APP_ID}.metainfo.xml"
# Step 5: Generate the Flatpak manifest
echo "[5/5] Generating Flatpak manifest..."
# Get current commit hash
COMMIT=$(git rev-parse HEAD)
# Generate the YAML manifest
cat > flathub/${APP_ID}.yml << 'MANIFEST_EOF'
id: su.reya.coop
runtime: org.freedesktop.Platform
runtime-version: "24.08"
sdk: org.freedesktop.Sdk
sdk-extensions:
- org.freedesktop.Sdk.Extension.rust-stable
- org.freedesktop.Sdk.Extension.llvm18
command: coop
finish-args:
- --talk-name=org.freedesktop.Flatpak
- --device=dri
- --share=ipc
- --share=network
- --socket=wayland
- --socket=fallback-x11
- --socket=pulseaudio
- --filesystem=host
build-options:
append-path: /usr/lib/sdk/rust-stable/bin:/usr/lib/sdk/llvm18/bin
env:
CC: clang
CXX: clang++
modules:
- name: coop
buildsystem: simple
build-options:
env:
CARGO_HOME: /run/build/coop/cargo
CARGO_NET_OFFLINE: "true"
RELEASE_VERSION: "@VERSION@"
build-commands:
# Setup vendored dependencies
- mkdir -p .cargo
- cp cargo-config.toml .cargo/config.toml
# Extract vendored deps
- tar -xzf vendor.tar.gz
# Build the project (entire workspace, then install coop binary)
- cargo build --release --offline --package coop
# Install binary
- install -Dm755 target/release/coop /app/bin/coop
# Install icons
- install -Dm644 crates/coop/resources/icon.png /app/share/icons/hicolor/512x512/apps/su.reya.coop.png
- install -Dm644 crates/coop/resources/icon@2x.png /app/share/icons/hicolor/1024x1024/apps/su.reya.coop.png
# Install desktop file
- |
export APP_ID="su.reya.coop"
export APP_ICON="su.reya.coop"
export APP_NAME="Coop"
export APP_CLI="coop"
export APP_ARGS="%U"
export DO_STARTUP_NOTIFY="true"
envsubst < crates/coop/resources/coop.desktop.in > coop.desktop
install -Dm644 coop.desktop /app/share/applications/su.reya.coop.desktop
# Install metainfo (use pre-generated one with release info)
- install -Dm644 su.reya.coop.metainfo.xml /app/share/metainfo/su.reya.coop.metainfo.xml
sources:
# Main source code - specific commit
- type: git
url: https://git.reya.su/reya/coop.git
commit: "@COMMIT@"
tag: "v@VERSION@"
# Vendored dependencies tarball (generated by this script)
- type: file
path: vendor.tar.gz
sha256: "@VENDOR_SHA256@"
# Pre-generated metainfo with release info
- type: file
path: su.reya.coop.metainfo.xml
sha256: "@METAINFO_SHA256@"
# Cargo config for vendoring
- type: file
path: cargo-config.toml
sha256: "@CARGO_CONFIG_SHA256@"
MANIFEST_EOF
# Calculate SHA256 hashes
VENDOR_SHA256=$(sha256sum flathub/vendor.tar.gz | cut -d' ' -f1)
METAINFO_SHA256=$(sha256sum flathub/${APP_ID}.metainfo.xml | cut -d' ' -f1)
# Create cargo-config.toml
mkdir -p flathub
cat > flathub/cargo-config.toml << 'EOF'
[source.crates-io]
replace-with = "vendored"
[source.vendored]
directory = "vendor"
EOF
CARGO_CONFIG_SHA256=$(sha256sum flathub/cargo-config.toml | cut -d' ' -f1)
# Substitute values into the manifest
sed -i \
-e "s/@VERSION@/${VERSION}/g" \
-e "s/@COMMIT@/${COMMIT}/g" \
-e "s/@VENDOR_SHA256@/${VENDOR_SHA256}/g" \
-e "s/@METAINFO_SHA256@/${METAINFO_SHA256}/g" \
-e "s/@CARGO_CONFIG_SHA256@/${CARGO_CONFIG_SHA256}/g" \
flathub/${APP_ID}.yml
echo " Created flathub/${APP_ID}.yml"
echo ""
echo "=== Flathub preparation complete! ==="
echo ""
echo "Files generated in flathub/:"
echo " - ${APP_ID}.yml # Main Flatpak manifest (submit this to Flathub)"
echo " - ${APP_ID}.metainfo.xml # AppStream metadata with release info"
echo " - vendor.tar.gz # Vendored Rust dependencies"
echo " - cargo-config.toml # Cargo configuration for vendoring"
echo " - release-info.xml # Release info snippet"
echo ""
echo "Next steps:"
echo " 1. Test the build locally:"
echo " cd flathub && flatpak-builder --user --install --force-clean build ${APP_ID}.yml"
echo ""
echo " 2. If build succeeds, submit to Flathub:"
echo " - Fork https://github.com/flathub/flathub"
echo " - Clone: git clone --branch=new-pr git@github.com:YOUR_USERNAME/flathub.git"
echo " - Copy ONLY ${APP_ID}.yml to the repo"
echo " - Submit PR against flathub/flathub:new-pr"
echo ""
echo "Note: Make sure you have:"
echo " - Committed all changes (commit: ${COMMIT})"
echo " - Tagged the release (tag: v${VERSION})"
echo " - Pushed the tag to GitHub"