mirror of
https://github.com/mikedilger/gossip.git
synced 2024-09-29 16:31:18 +00:00
Merge remote-tracking branch 'dilger/unstable' into feature/finish-priority-relay-ui
# Conflicts: # gossip-bin/src/ui/widgets/mod.rs
This commit is contained in:
commit
0e72b40556
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@
|
||||
perf.data
|
||||
perf.data.old
|
||||
flamegraph.svg
|
||||
.vscode/
|
||||
|
50
README.md
50
README.md
@ -1,8 +1,8 @@
|
||||
# Gossip
|
||||
|
||||
## Gossip is a desktop client for nostr.
|
||||
## Gossip is a desktop client for NOSTR
|
||||
|
||||
Nostr is an open social media protocol empowering lots of software such as this client. The experience is kind of like Twitter except that you control your own account, and you can post to many different independent places called "relays". People are finding many additional uses for nostr that go far beyond micro-blogging or chatting, but this client is focused on those.
|
||||
Nostr is an open social media protocol empowering lots of software such as this client. The experience is kind of like Twitter except that you control your own account, and you can post to many different independent places called "relays". People are finding many additional uses for NOSTR that go far beyond micro-blogging or chatting, but this client is focused on those.
|
||||
|
||||
Nostr stands for "Notes and Other Stuff Transmitted by Relays."
|
||||
|
||||
@ -55,6 +55,7 @@ The following features make gossip different than most other nostr clients so fa
|
||||
- [x] NIP-21 - nostr: URL scheme
|
||||
- [x] NIP-22 - Event created_at Limits
|
||||
- [ ] NIP-23 - Long-form Content [Optional viewing, but not creating]
|
||||
- [ ] NIP-24 - Extra metadata fields and tags
|
||||
- [x] NIP-25 - Reactions
|
||||
- [x] NIP-26 - Delegated Event Signing
|
||||
- [x] NIP-27 - Text Note References
|
||||
@ -91,8 +92,8 @@ The following features make gossip different than most other nostr clients so fa
|
||||
If when you pull gossip it doesn't pull cleanly, I may have done a rare force-push. Run these commands to reset your master branch:
|
||||
|
||||
````bash
|
||||
$ git fetch
|
||||
$ git reset --hard origin/master
|
||||
git fetch
|
||||
git reset --hard origin/master
|
||||
````
|
||||
|
||||
### Step 1 - Install Rust
|
||||
@ -112,35 +113,35 @@ Most dependencies are probably already installed in your base operating system.
|
||||
|
||||
#### macOS
|
||||
|
||||
a. Install rust with rust-up: https://rustup.rs/
|
||||
b. Install homebrew if you don't have it yet https://brew.sh/
|
||||
a. Install rust with rust-up: <https://rustup.rs/>
|
||||
b. Install homebrew if you don't have it yet <https://brew.sh/>
|
||||
c. Install these dependencies:
|
||||
|
||||
```
|
||||
```bash
|
||||
brew install cmake sdl2 pkg-config ffmpeg
|
||||
```
|
||||
|
||||
### Step 3 - Clone this Repository
|
||||
|
||||
````bash
|
||||
$ git clone https://github.com/mikedilger/gossip
|
||||
git clone https://github.com/mikedilger/gossip
|
||||
````
|
||||
|
||||
### Step 4 - Compile
|
||||
|
||||
````bash
|
||||
$ cd gossip
|
||||
$ cargo build --release
|
||||
cd gossip
|
||||
cargo build --release
|
||||
````
|
||||
|
||||
The output will be a binary executable in `target/release/gossip`
|
||||
|
||||
This binary should be portable to similar systems with similar hardware and operating system.
|
||||
|
||||
If you want a binary optimized for your exact processor with the newest features enabled:
|
||||
If you want a binary optimized for your exact processor with the newest CPU features enabled, and all gossip features enabled:
|
||||
|
||||
````bash
|
||||
$ RUSTFLAGS="-C target-cpu=native --cfg tokio_unstable" cargo build --release
|
||||
RUSTFLAGS="-C target-cpu=native --cfg tokio_unstable" cargo build --features=lang-cjk,video-ffmpeg --release
|
||||
````
|
||||
|
||||
Everything gossip needs (fonts, icons) is baked into this executable. It doesn't need to find assets. So you can move it and run it from anywhere.
|
||||
@ -148,7 +149,7 @@ Everything gossip needs (fonts, icons) is baked into this executable. It doesn't
|
||||
To make the binary smaller,
|
||||
|
||||
````bash
|
||||
$ strip gossip
|
||||
strip gossip
|
||||
````
|
||||
|
||||
### Step 5 - Do it all again
|
||||
@ -156,10 +157,10 @@ $ strip gossip
|
||||
The `master` branch changes quickly. When you want to update, do it all again, something like this:
|
||||
|
||||
````bash
|
||||
$ git pull
|
||||
$ cargo build --release
|
||||
$ strip ./target/release/gossip
|
||||
$ ./target/release/gossip
|
||||
git pull
|
||||
cargo build --release
|
||||
strip ./target/release/gossip
|
||||
./target/release/gossip
|
||||
````
|
||||
|
||||
## Compile Options
|
||||
@ -179,7 +180,6 @@ If you wish to switch to your native TLS provider, use the following compile opt
|
||||
|
||||
### Language Support
|
||||
|
||||
|
||||
#### Chinese, Japanese and Korean character sets
|
||||
|
||||
Gossip by default does not include the CJK font because it is larger than all other languages put together, and most gossip users don't recognize those characters. If you do recognize such characters, you can compile in that font with:
|
||||
@ -206,7 +206,7 @@ Compile with
|
||||
|
||||
### Performance issues
|
||||
|
||||
If you are having performance issues, please see [PERFORMANCE.md](PERFORMANCE.md).
|
||||
If you are having performance issues, please see [PERFORMANCE.md](docs/PERFORMANCE.md).
|
||||
|
||||
## Technology Involved
|
||||
|
||||
@ -220,11 +220,13 @@ If you are having performance issues, please see [PERFORMANCE.md](PERFORMANCE.md
|
||||
|
||||
## License
|
||||
|
||||
* MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
|
||||
MIT license ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)
|
||||
|
||||
### Contribution
|
||||
|
||||
All contributions welcome, please check the [development guidelines](DEVELOPING.md) before starting to code.
|
||||
All contributions welcome, please check the [development guidelines](docs/DEVELOPING.md) before starting to code.
|
||||
|
||||
Please join [Gossip Telegram Channel](https://t.me/gossipclient).
|
||||
|
||||
Anyone interested in replacing the GUI with something much better, or keeping it as egui but making it much better, would be greatly appreciated.
|
||||
|
||||
@ -232,13 +234,13 @@ Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
|
||||
## On Nostr
|
||||
|
||||
### The official gossip account:
|
||||
### The official gossip account
|
||||
|
||||
nprofile1qqsrjerj9rhamu30sjnuudk3zxeh3njl852mssqng7z4up9jfj8yupqpzamhxue69uhhyetvv9ujumn0wd68ytnfdenx7tcpz4mhxue69uhkummnw3ezummcw3ezuer9wchszxmhwden5te0dehhxarj9ekkj6m9v35kcem9wghxxmmd9uq3xamnwvaz7tm0venxx6rpd9hzuur4vghsz8nhwden5te0dehhxarj94c82c3wwajkcmr0wfjx2u3wdejhgtcsfx2xk
|
||||
|
||||
npub189j8y280mhezlp98ecmdzydn0r8970g4hpqpx3u9tcztynywfczqqr3tg8
|
||||
|
||||
### Mike Dilger:
|
||||
### Mike Dilger
|
||||
|
||||
nprofile1qqswuyd9ml6qcxd92h6pleptfrcqucvvjy39vg4wx7mv9wm8kakyujgpzamhxue69uhhyetvv9ujumn0wd68ytnfdenx7tcprpmhxue69uhkzapwdehhxarjwahhy6mn9e3k7mf0qyt8wumn8ghj7etyv4hzumn0wd68ytnvv9hxgtcprdmhxue69uhkummnw3ezumtfddjkg6tvvajhytnrdakj7qgnwaehxw309ahkvenrdpskjm3wwp6kytcpremhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet59uq32amnwvaz7tmwdaehgu3wdau8gu3wv3jhvtct8l34m
|
||||
|
||||
@ -248,4 +250,4 @@ You can also my NIP-05 address of `mike@mikedilger.com` which will also hook you
|
||||
|
||||
I'd prefer if you trusted `mike@mikedilger.com` higher than my public key at this point in time since key management is still pretty bad. That is the inverse of the normal recommendation, but my private key has not been treated very carefully as I never intended it to be my long-term keypair (it just became that over time). Also, I fully intend to rollover my keys once gossip supports the key-rollover NIP, whatever that is (or will be).
|
||||
|
||||
You can tip me at my Bitcoin Lighting address: decentbun13@walletofsatoshi.com == lnurl1dp68gurn8ghj7ampd3kx2ar0veekzar0wd5xjtnrdakj7tnhv4kxctttdehhwm30d3h82unvwqhkgetrv4h8gcn4dccnxv563ep
|
||||
You can tip me at my Bitcoin Lighting address: <decentbun13@walletofsatoshi.com> == lnurl1dp68gurn8ghj7ampd3kx2ar0veekzar0wd5xjtnrdakj7tnhv4kxctttdehhwm30d3h82unvwqhkgetrv4h8gcn4dccnxv563ep
|
||||
|
@ -1,4 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
cargo build --features=lang-cjk,video-ffmpeg && \
|
||||
RUST_BACKTRACE=1 RUST_LOG="info,gossip=debug" ./target/debug/gossip
|
@ -4,18 +4,20 @@ Gossip is architected with the following components:
|
||||
|
||||
- A User Interface thread, synchronous
|
||||
- Tokio asynchronous runtime running
|
||||
- An overlord (handles most jobs)
|
||||
- A set of minions (each one handles one relay)
|
||||
- An overlord (handles most jobs)
|
||||
- A set of minions (each one handles one relay)
|
||||
|
||||
## Keeping the UI responsive
|
||||
|
||||
The most important thing to be aware of is that the User Interface thread repeatedly calculates what to draw and potentially redraws up to 60 frames per second, therefore it **must** not run any slow code.
|
||||
|
||||
To that end, the following are allowed from the UI thread:
|
||||
|
||||
- Locking global variables (since nearly all locks in gossip are intended to be rapidly released)
|
||||
- Sending messages to the overlord.
|
||||
|
||||
The following is NOT appreciated when done from the UI thread:
|
||||
|
||||
- Database calls, or calls to functions that do database calls
|
||||
- Internet queries, or calls to functions that query over the Internet
|
||||
|
||||
@ -31,7 +33,8 @@ The overlord generally is the one to send messages to minions using the GLOBALS.
|
||||
|
||||
## Flow
|
||||
|
||||
The flow generally happens like this
|
||||
The flow generally happens like this:
|
||||
|
||||
- The user interacts with the UI
|
||||
- The UI requests something of the Overlord
|
||||
- The overlord either does it, or spawns a task to do it if it takes too long (the overlord should also remain somewhat responsive).
|
||||
@ -45,16 +48,16 @@ The flow generally happens like this
|
||||
|
||||
## Pull Requests
|
||||
|
||||
I prefer that you run and make pass
|
||||
I prefer that you run and make pass:
|
||||
|
||||
````sh
|
||||
$ cargo clippy
|
||||
````bash
|
||||
cargo clippy
|
||||
````
|
||||
|
||||
and then
|
||||
|
||||
````sh
|
||||
$ cargo fmt
|
||||
````bash
|
||||
cargo fmt
|
||||
````
|
||||
|
||||
before you issue a pull request. Otherwise I'll have to do it for you.
|
15
docs/DOCUMENTATION.md
Normal file
15
docs/DOCUMENTATION.md
Normal file
@ -0,0 +1,15 @@
|
||||
# DOCUMENTATION
|
||||
|
||||
You may want to generate the Gossip Rust Documentation from root folder:
|
||||
|
||||
````bash
|
||||
cargo doc --lib
|
||||
````
|
||||
|
||||
The output in `target/doc/gossip_lib/index.html` may be browsed.
|
||||
|
||||
For lasiest people the following will directly open the browser:
|
||||
|
||||
````bash
|
||||
cargo doc --lib --open
|
||||
````
|
@ -41,7 +41,7 @@ This issue will be ameliorated somewhat in the future when you can have differen
|
||||
Gossip should be compiled in release mode. You can also compile it for your individual processor to squeeze out the most performance (the following line leaves out feature flags, you'll wnat to determine which ones are right for you):
|
||||
|
||||
````bash
|
||||
$ RUSTFLAGS="-C target-cpu=native --cfg tokio_unstable" cargo build --release
|
||||
RUSTFLAGS="-C target-cpu=native --cfg tokio_unstable" cargo build --release
|
||||
````
|
||||
|
||||
### Dumb Programmers
|
@ -1,4 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
find . -name .git -prune -o -name target -prune -o -type f -exec grep -H "$1" {} \; \
|
||||
| awk -F: '{print $1}' | uniq
|
3
find.sh
3
find.sh
@ -1,3 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
find . -name .git -prune -o -name target -prune -o -type f -exec grep -H "$1" {} \;
|
@ -1,4 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
find . -name .git -prune -o -name target -prune -o -name "$1" -print
|
||||
|
@ -18,7 +18,7 @@ rustls-tls = [ "gossip-lib/rustls-tls" ]
|
||||
|
||||
[dependencies]
|
||||
bech32 = "0.9"
|
||||
eframe = { git = "https://github.com/mikedilger/egui", rev = "50393e4f34ac6246b8c2424e42fbe5b95e4b4452", features = [ "persistence" ] }
|
||||
eframe = { git = "https://github.com/mikedilger/egui", rev = "50393e4f34ac6246b8c2424e42fbe5b95e4b4452", features = [ "persistence", "wayland" ] }
|
||||
egui-winit = { git = "https://github.com/mikedilger/egui", rev = "50393e4f34ac6246b8c2424e42fbe5b95e4b4452", features = [ "default" ] }
|
||||
egui-video = { git = "https://github.com/mikedilger/egui-video", rev = "81cc3ee58818754272582397161cc55ff11bde18", features = [ "from_bytes" ], optional = true }
|
||||
gossip-relay-picker = { git = "https://github.com/mikedilger/gossip-relay-picker", rev = "39f9c14b1c201842c512754920f4da4987cd68b6" }
|
||||
|
8
gossip-bin/build.rs
Normal file
8
gossip-bin/build.rs
Normal file
@ -0,0 +1,8 @@
|
||||
fn main() {
|
||||
// link to bundled libraries
|
||||
#[cfg(target_os = "macos")]
|
||||
println!("cargo:rustc-link-arg=-Wl,-rpath,@loader_path");
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
println!("cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN");
|
||||
}
|
@ -4,10 +4,18 @@ use egui::{Context, Label, RichText, Sense, Ui};
|
||||
use gossip_lib::DmChannelData;
|
||||
use gossip_lib::FeedKind;
|
||||
use gossip_lib::GLOBALS;
|
||||
use gossip_lib::{Error, ErrorKind};
|
||||
|
||||
pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
|
||||
let mut channels: Vec<DmChannelData> = match GLOBALS.storage.dm_channels() {
|
||||
Ok(channels) => channels,
|
||||
Err(Error {
|
||||
kind: ErrorKind::NoPrivateKey,
|
||||
..
|
||||
}) => {
|
||||
ui.label("Private Key Not Available");
|
||||
return;
|
||||
}
|
||||
Err(_) => {
|
||||
ui.label("ERROR");
|
||||
return;
|
||||
@ -32,14 +40,16 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr
|
||||
));
|
||||
|
||||
ui.label(
|
||||
RichText::new(crate::date_ago::date_ago(channeldata.latest_message))
|
||||
.italics()
|
||||
.weak(),
|
||||
RichText::new(crate::date_ago::date_ago(
|
||||
channeldata.latest_message_created_at,
|
||||
))
|
||||
.italics()
|
||||
.weak(),
|
||||
)
|
||||
.on_hover_ui(|ui| {
|
||||
if let Ok(stamp) =
|
||||
time::OffsetDateTime::from_unix_timestamp(channeldata.latest_message.0)
|
||||
{
|
||||
if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(
|
||||
channeldata.latest_message_created_at.0,
|
||||
) {
|
||||
if let Ok(formatted) =
|
||||
stamp.format(&time::format_description::well_known::Rfc2822)
|
||||
{
|
||||
|
@ -55,17 +55,17 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram
|
||||
FeedKind::Followed(with_replies) => {
|
||||
let feed = GLOBALS.feed.get_followed();
|
||||
let id = if with_replies { "main" } else { "general" };
|
||||
|
||||
ui.add_space(10.0);
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2::new(ui.available_width(), ui.spacing().interact_size.y),
|
||||
egui::Layout::left_to_right(egui::Align::Center),
|
||||
|ui| {
|
||||
add_left_space(ui);
|
||||
ui.label("Main Feed");
|
||||
ui.heading("Main feed");
|
||||
recompute_btn(app, ui);
|
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.add_space(16.0);
|
||||
ui.add_space(10.0);
|
||||
ui.label(RichText::new("Include replies").size(11.0));
|
||||
let size = ui.spacing().interact_size.y * egui::vec2(1.6, 0.8);
|
||||
if crate::ui::components::switch_with_size(
|
||||
@ -89,7 +89,7 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram
|
||||
});
|
||||
},
|
||||
);
|
||||
ui.add_space(4.0);
|
||||
ui.add_space(6.0);
|
||||
render_a_feed(app, ctx, frame, ui, feed, false, id);
|
||||
}
|
||||
FeedKind::Inbox(indirect) => {
|
||||
@ -104,17 +104,17 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram
|
||||
}
|
||||
let feed = GLOBALS.feed.get_inbox();
|
||||
let id = if indirect { "activity" } else { "inbox" };
|
||||
|
||||
ui.add_space(10.0);
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2::new(ui.available_width(), ui.spacing().interact_size.y),
|
||||
egui::Layout::left_to_right(egui::Align::Center),
|
||||
|ui| {
|
||||
add_left_space(ui);
|
||||
ui.label("Inbox");
|
||||
ui.heading("Inbox");
|
||||
recompute_btn(app, ui);
|
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.add_space(16.0);
|
||||
ui.add_space(10.0);
|
||||
ui.label(RichText::new("Everything").size(11.0));
|
||||
let size = ui.spacing().interact_size.y * egui::vec2(1.6, 0.8);
|
||||
if crate::ui::components::switch_with_size(
|
||||
@ -136,23 +136,26 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram
|
||||
});
|
||||
},
|
||||
);
|
||||
ui.add_space(4.0);
|
||||
ui.add_space(6.0);
|
||||
render_a_feed(app, ctx, frame, ui, feed, false, id);
|
||||
}
|
||||
FeedKind::Thread { id, .. } => {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Thread");
|
||||
recompute_btn(app, ui);
|
||||
});
|
||||
if let Some(parent) = GLOBALS.feed.get_thread_parent() {
|
||||
render_a_feed(app, ctx, frame, ui, vec![parent], true, &id.as_hex_string());
|
||||
}
|
||||
}
|
||||
FeedKind::Person(pubkey) => {
|
||||
ui.add_space(10.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(gossip_lib::names::tag_name_from_pubkey_lookup(&pubkey));
|
||||
add_left_space(ui);
|
||||
if Some(pubkey) == GLOBALS.signer.public_key() {
|
||||
ui.heading("My notes");
|
||||
} else {
|
||||
ui.heading(gossip_lib::names::tag_name_from_pubkey_lookup(&pubkey));
|
||||
}
|
||||
recompute_btn(app, ui);
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
|
||||
let feed = GLOBALS.feed.get_person_feed();
|
||||
render_a_feed(app, ctx, frame, ui, feed, false, &pubkey.as_hex_string());
|
||||
|
@ -284,7 +284,7 @@ fn render_note_inner(
|
||||
|
||||
ui.add_space(3.0);
|
||||
|
||||
GossipUi::render_person_name_line(app, ui, ¬e.author);
|
||||
GossipUi::render_person_name_line(app, ui, ¬e.author, false);
|
||||
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
if let Some((irt, _)) = note.event.replies_to() {
|
||||
@ -306,30 +306,51 @@ fn render_note_inner(
|
||||
ui.add_space(8.0);
|
||||
|
||||
if note.event.pow() > 0 {
|
||||
ui.label(format!("POW={}", note.event.pow()));
|
||||
let color = app.theme.notice_marker_text_color();
|
||||
ui.label(
|
||||
RichText::new(format!("POW={}", note.event.pow()))
|
||||
.color(color)
|
||||
.text_style(TextStyle::Small),
|
||||
);
|
||||
}
|
||||
|
||||
match ¬e.delegation {
|
||||
EventDelegation::InvalidDelegation(why) => {
|
||||
let color = app.theme.warning_marker_text_color();
|
||||
ui.add(Label::new(RichText::new("INVALID DELEGATION").color(color)))
|
||||
.on_hover_text(why);
|
||||
ui.add(Label::new(
|
||||
RichText::new("INVALID DELEGATION")
|
||||
.color(color)
|
||||
.text_style(TextStyle::Small),
|
||||
))
|
||||
.on_hover_text(why);
|
||||
}
|
||||
EventDelegation::DelegatedBy(_) => {
|
||||
let color = app.theme.notice_marker_text_color();
|
||||
ui.label(RichText::new("DELEGATED").color(color));
|
||||
ui.label(
|
||||
RichText::new("DELEGATED")
|
||||
.color(color)
|
||||
.text_style(TextStyle::Small),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if note.deletion.is_some() {
|
||||
let color = app.theme.warning_marker_text_color();
|
||||
ui.label(RichText::new("DELETED").color(color));
|
||||
ui.label(
|
||||
RichText::new("DELETED")
|
||||
.color(color)
|
||||
.text_style(TextStyle::Small),
|
||||
);
|
||||
}
|
||||
|
||||
if note.event.kind == EventKind::Repost {
|
||||
let color = app.theme.notice_marker_text_color();
|
||||
ui.label(RichText::new("REPOSTED").color(color));
|
||||
ui.label(
|
||||
RichText::new("REPOSTED")
|
||||
.color(color)
|
||||
.text_style(TextStyle::Small),
|
||||
);
|
||||
}
|
||||
|
||||
if let Page::Feed(FeedKind::DmChat(_)) = app.page {
|
||||
@ -338,9 +359,17 @@ fn render_note_inner(
|
||||
if note.event.kind.is_direct_message_related() {
|
||||
let color = app.theme.notice_marker_text_color();
|
||||
if note.secure {
|
||||
ui.label(RichText::new("Private Chat (Gift Wrapped)").color(color));
|
||||
ui.label(
|
||||
RichText::new("PRIVATE CHAT (GIFT WRAPPED)")
|
||||
.color(color)
|
||||
.text_style(TextStyle::Small),
|
||||
);
|
||||
} else {
|
||||
ui.label(RichText::new("Private Chat").color(color));
|
||||
ui.label(
|
||||
RichText::new("PRIVATE CHAT")
|
||||
.color(color)
|
||||
.text_style(TextStyle::Small),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ mod theme;
|
||||
|
||||
pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
|
||||
if app.page == Page::HelpHelp {
|
||||
ui.add_space(24.0);
|
||||
ui.add_space(10.0);
|
||||
ui.heading("Help - Getting Started");
|
||||
ui.add_space(12.0);
|
||||
ui.separator();
|
||||
|
@ -6,7 +6,7 @@ use humansize::{format_size, DECIMAL};
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
|
||||
ui.add_space(24.0);
|
||||
ui.add_space(10.0);
|
||||
ui.heading("Statistics".to_string());
|
||||
ui.add_space(12.0);
|
||||
ui.separator();
|
||||
@ -77,11 +77,8 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr
|
||||
ui.add_space(6.0);
|
||||
|
||||
ui.label(format!(
|
||||
"Event Index (References Person): {} records",
|
||||
GLOBALS
|
||||
.storage
|
||||
.get_event_references_person_len()
|
||||
.unwrap_or(0)
|
||||
"Event Index (Tags): {} records",
|
||||
GLOBALS.storage.get_event_tag_index_len().unwrap_or(0)
|
||||
));
|
||||
ui.add_space(6.0);
|
||||
|
||||
|
@ -15,7 +15,7 @@ enum Background {
|
||||
}
|
||||
|
||||
pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
|
||||
ui.add_space(24.0);
|
||||
ui.add_space(10.0);
|
||||
ui.heading("Theme Test".to_string());
|
||||
ui.add_space(12.0);
|
||||
ui.separator();
|
||||
|
@ -56,11 +56,13 @@ use self::widgets::NavItem;
|
||||
use self::wizard::{WizardPage, WizardState};
|
||||
|
||||
pub fn run() -> Result<(), Error> {
|
||||
let icon_bytes = include_bytes!("../../../gossip.png");
|
||||
let icon_bytes = include_bytes!("../../../logo/gossip.png");
|
||||
let icon = image::load_from_memory(icon_bytes)?.to_rgba8();
|
||||
let (icon_width, icon_height) = icon.dimensions();
|
||||
|
||||
let options = eframe::NativeOptions {
|
||||
#[cfg(target_os = "linux")]
|
||||
app_id: Some("gossip".to_string()),
|
||||
decorated: true,
|
||||
#[cfg(target_os = "macos")]
|
||||
fullsize_content: true,
|
||||
@ -479,7 +481,7 @@ impl GossipUi {
|
||||
submenu_ids.insert(SubMenu::Help, egui::Id::new(SubMenu::Help.to_id_str()));
|
||||
|
||||
let icon_texture_handle = {
|
||||
let bytes = include_bytes!("../../../gossip.png");
|
||||
let bytes = include_bytes!("../../../logo/gossip.png");
|
||||
let image = image::load_from_memory(bytes).unwrap();
|
||||
let size = [image.width() as _, image.height() as _];
|
||||
let image_buffer = image.to_rgba8();
|
||||
@ -732,7 +734,7 @@ impl GossipUi {
|
||||
|
||||
ui.add_space(4.0);
|
||||
let back_label_text = RichText::new("‹ Back");
|
||||
let label = if self.history.is_empty() { Label::new(back_label_text.color(Color32::from_white_alpha(8))) } else { Label::new(back_label_text.color(self.theme.navigation_text_color())).sense(Sense::click()) };
|
||||
let label = if self.history.is_empty() { Label::new(back_label_text.color(self.theme.navigation_text_deactivated_color())) } else { Label::new(back_label_text.color(self.theme.navigation_text_color())).sense(Sense::click()) };
|
||||
let response = ui.add(label);
|
||||
let response = if let Some(page) = self.history.last() {
|
||||
response.on_hover_text(format!("back to {}", page.to_short_string()))
|
||||
@ -887,7 +889,7 @@ impl GossipUi {
|
||||
.shadow(egui::epaint::Shadow::NONE)
|
||||
.show(ui, |ui| {
|
||||
let text = if GLOBALS.signer.is_ready() { RichText::new("+").size(22.5) } else { RichText::new("\u{1f513}").size(20.0) };
|
||||
let response = ui.add_sized([crate::AVATAR_SIZE_F32, crate::AVATAR_SIZE_F32], egui::Button::new(text.color(self.theme.navigation_text_color())).stroke(egui::Stroke::NONE).rounding(egui::Rounding::same(crate::AVATAR_SIZE_F32)).fill(self.theme.navigation_bg_fill()));
|
||||
let response = ui.add_sized([crate::AVATAR_SIZE_F32, crate::AVATAR_SIZE_F32], egui::Button::new(text.color(self.theme.get_style().visuals.panel_fill)).stroke(egui::Stroke::NONE).rounding(egui::Rounding::same(crate::AVATAR_SIZE_F32)).fill(self.theme.accent_color()));
|
||||
if response.clicked() {
|
||||
self.show_post_area = true;
|
||||
if GLOBALS.signer.is_ready() {
|
||||
@ -971,15 +973,25 @@ impl eframe::App for GossipUi {
|
||||
self.current_scroll_offset = requested_scroll;
|
||||
}
|
||||
|
||||
if self.theme.follow_os_dark_mode {
|
||||
let mut reapply = false;
|
||||
let mut theme = Theme::from_settings(&self.settings);
|
||||
if theme.follow_os_dark_mode {
|
||||
// detect if the OS has changed dark/light mode
|
||||
let os_dark_mode = ctx.style().visuals.dark_mode;
|
||||
if os_dark_mode != self.theme.dark_mode {
|
||||
if os_dark_mode != theme.dark_mode {
|
||||
// switch to the OS setting
|
||||
self.theme.dark_mode = os_dark_mode;
|
||||
theme::apply_theme(&self.theme, ctx);
|
||||
self.settings.dark_mode = os_dark_mode;
|
||||
theme.dark_mode = os_dark_mode;
|
||||
reapply = true;
|
||||
}
|
||||
}
|
||||
if self.theme != theme {
|
||||
self.theme = theme;
|
||||
reapply = true;
|
||||
}
|
||||
if reapply {
|
||||
theme::apply_theme(&self.theme, ctx);
|
||||
}
|
||||
|
||||
// dialogues first
|
||||
if relays::is_entry_dialog_active(self) {
|
||||
@ -1098,7 +1110,12 @@ impl GossipUi {
|
||||
ui.set_enabled(!relays::is_entry_dialog_active(self));
|
||||
}
|
||||
|
||||
pub fn render_person_name_line(app: &mut GossipUi, ui: &mut Ui, person: &Person) {
|
||||
pub fn render_person_name_line(
|
||||
app: &mut GossipUi,
|
||||
ui: &mut Ui,
|
||||
person: &Person,
|
||||
profile_page: bool,
|
||||
) {
|
||||
// Let the 'People' manager know that we are interested in displaying this person.
|
||||
// It will take all actions necessary to make the data eventually available.
|
||||
GLOBALS.people.person_of_interest(person.pubkey);
|
||||
@ -1113,16 +1130,22 @@ impl GossipUi {
|
||||
};
|
||||
|
||||
let tag_name_menu = {
|
||||
let text = match &person.petname {
|
||||
Some(pn) => pn.to_owned(),
|
||||
None => gossip_lib::names::tag_name_from_person(person),
|
||||
let text = if !profile_page {
|
||||
match &person.petname {
|
||||
Some(pn) => pn.to_owned(),
|
||||
None => gossip_lib::names::tag_name_from_person(person),
|
||||
}
|
||||
} else {
|
||||
"ACTIONS".to_string()
|
||||
};
|
||||
RichText::new(format!("☰ {}", text))
|
||||
};
|
||||
|
||||
ui.menu_button(tag_name_menu, |ui| {
|
||||
if ui.button("View Person").clicked() {
|
||||
app.set_page(Page::Person(person.pubkey));
|
||||
if !profile_page {
|
||||
if ui.button("View Person").clicked() {
|
||||
app.set_page(Page::Person(person.pubkey));
|
||||
}
|
||||
}
|
||||
if app.page != Page::Feed(FeedKind::Person(person.pubkey)) {
|
||||
if ui.button("View Their Posts").clicked() {
|
||||
@ -1181,23 +1204,25 @@ impl GossipUi {
|
||||
.on_hover_text("followed");
|
||||
}
|
||||
|
||||
if let Some(mut nip05) = person.nip05().map(|s| s.to_owned()) {
|
||||
if nip05.starts_with("_@") {
|
||||
nip05 = nip05.get(2..).unwrap().to_string();
|
||||
}
|
||||
if !profile_page {
|
||||
if let Some(mut nip05) = person.nip05().map(|s| s.to_owned()) {
|
||||
if nip05.starts_with("_@") {
|
||||
nip05 = nip05.get(2..).unwrap().to_string();
|
||||
}
|
||||
|
||||
ui.with_layout(
|
||||
Layout::left_to_right(Align::Min)
|
||||
.with_cross_align(Align::Center)
|
||||
.with_cross_justify(true),
|
||||
|ui| {
|
||||
if person.nip05_valid {
|
||||
ui.label(RichText::new(nip05).monospace().small());
|
||||
} else {
|
||||
ui.label(RichText::new(nip05).monospace().small().strikethrough());
|
||||
}
|
||||
},
|
||||
);
|
||||
ui.with_layout(
|
||||
Layout::left_to_right(Align::Min)
|
||||
.with_cross_align(Align::Center)
|
||||
.with_cross_justify(true),
|
||||
|ui| {
|
||||
if person.nip05_valid {
|
||||
ui.label(RichText::new(nip05).monospace().small());
|
||||
} else {
|
||||
ui.label(RichText::new(nip05).monospace().small().strikethrough());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -6,9 +6,12 @@ use gossip_lib::GLOBALS;
|
||||
use nostr_types::{Profile, PublicKey};
|
||||
|
||||
pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
|
||||
ui.add_space(30.0);
|
||||
ui.add_space(10.0);
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.add_space(2.0);
|
||||
ui.heading("Follow Someone");
|
||||
});
|
||||
|
||||
ui.heading("Follow Someone");
|
||||
ui.add_space(10.0);
|
||||
|
||||
ui.label(
|
||||
|
@ -181,7 +181,7 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.label(RichText::new(gossip_lib::names::pubkey_short(&person.pubkey)).weak());
|
||||
GossipUi::render_person_name_line(app, ui, person);
|
||||
GossipUi::render_person_name_line(app, ui, person, false);
|
||||
if !GLOBALS
|
||||
.storage
|
||||
.have_persons_relays(person.pubkey)
|
||||
|
@ -9,10 +9,15 @@ use std::sync::atomic::Ordering;
|
||||
|
||||
pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
|
||||
let muted_pubkeys = GLOBALS.people.get_muted_pubkeys();
|
||||
|
||||
let mut people: Vec<Person> = Vec::new();
|
||||
for pk in &muted_pubkeys {
|
||||
if let Ok(Some(person)) = GLOBALS.storage.read_person(pk) {
|
||||
people.push(person);
|
||||
} else {
|
||||
let person = Person::new(pk.to_owned());
|
||||
let _ = GLOBALS.storage.write_person(&person, None);
|
||||
people.push(person);
|
||||
}
|
||||
}
|
||||
people.sort_unstable();
|
||||
@ -149,7 +154,7 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
|
||||
|
||||
ui.vertical(|ui| {
|
||||
ui.label(RichText::new(gossip_lib::names::pubkey_short(&person.pubkey)).weak());
|
||||
GossipUi::render_person_name_line(app, ui, person);
|
||||
GossipUi::render_person_name_line(app, ui, person, false);
|
||||
|
||||
if ui.button("UNMUTE").clicked() {
|
||||
let _ = GLOBALS.people.mute(&person.pubkey, false);
|
||||
|
@ -2,7 +2,7 @@ use super::{GossipUi, Page};
|
||||
use crate::ui::widgets::CopyButton;
|
||||
use crate::AVATAR_SIZE_F32;
|
||||
use eframe::egui;
|
||||
use egui::{Context, Frame, Image, RichText, TextEdit, Ui, Vec2};
|
||||
use egui::{Context, Image, RichText, TextEdit, Ui, Vec2};
|
||||
use gossip_lib::comms::ToOverlordMessage;
|
||||
use gossip_lib::Person;
|
||||
use gossip_lib::GLOBALS;
|
||||
@ -34,63 +34,51 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
|
||||
}
|
||||
|
||||
fn content(app: &mut GossipUi, ctx: &Context, ui: &mut Ui, pubkey: PublicKey, person: Person) {
|
||||
ui.add_space(24.0);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
// Avatar first
|
||||
let avatar = if let Some(avatar) = app.try_get_avatar(ctx, &pubkey) {
|
||||
avatar
|
||||
} else {
|
||||
app.placeholder_avatar.clone()
|
||||
};
|
||||
ui.add(
|
||||
Image::new(&avatar)
|
||||
.max_size(Vec2 {
|
||||
x: AVATAR_SIZE_F32 * 3.0,
|
||||
y: AVATAR_SIZE_F32 * 3.0,
|
||||
})
|
||||
.maintain_aspect_ratio(true),
|
||||
);
|
||||
ui.vertical(|ui| {
|
||||
let display_name = gossip_lib::names::display_name_from_person(&person);
|
||||
ui.heading(display_name);
|
||||
ui.label(RichText::new(gossip_lib::names::pubkey_short(&pubkey)).weak());
|
||||
GossipUi::render_person_name_line(app, ui, &person);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(12.0);
|
||||
ui.label("Pet name:");
|
||||
if app.editing_petname {
|
||||
let edit_color = app.theme.input_text_color();
|
||||
ui.add(TextEdit::singleline(&mut app.petname).text_color(edit_color));
|
||||
if ui.button("save").clicked() {
|
||||
let mut person = person.clone();
|
||||
person.petname = Some(app.petname.clone());
|
||||
if let Err(e) = GLOBALS.storage.write_person(&person, None) {
|
||||
GLOBALS.status_queue.write().write(format!("{}", e));
|
||||
}
|
||||
app.editing_petname = false;
|
||||
app.notes.cache_invalidate_person(&person.pubkey);
|
||||
}
|
||||
if ui.button("cancel").clicked() {
|
||||
app.editing_petname = false;
|
||||
}
|
||||
if ui.button("remove").clicked() {
|
||||
let mut person = person.clone();
|
||||
person.petname = None;
|
||||
if let Err(e) = GLOBALS.storage.write_person(&person, None) {
|
||||
GLOBALS.status_queue.write().write(format!("{}", e));
|
||||
}
|
||||
app.editing_petname = false;
|
||||
app.notes.cache_invalidate_person(&person.pubkey);
|
||||
}
|
||||
} else {
|
||||
match &person.petname {
|
||||
Some(pn) => {
|
||||
ui.label(pn);
|
||||
if ui.button("edit").clicked() {
|
||||
app.editing_petname = true;
|
||||
app.petname = pn.to_owned();
|
||||
ui.vertical(|ui| {
|
||||
ui.add_space(10.0);
|
||||
ui.allocate_ui_with_layout(
|
||||
Vec2::new(ui.available_width(), ui.spacing().interact_size.y),
|
||||
egui::Layout::right_to_left(egui::Align::Center),
|
||||
|ui| {
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let avatar = if let Some(avatar) = app.try_get_avatar(ctx, &pubkey) {
|
||||
avatar
|
||||
} else {
|
||||
app.placeholder_avatar.clone()
|
||||
};
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(20.0);
|
||||
ui.add(
|
||||
Image::new(&avatar)
|
||||
.max_size(Vec2 {
|
||||
x: AVATAR_SIZE_F32 * 3.0,
|
||||
y: AVATAR_SIZE_F32 * 3.0,
|
||||
})
|
||||
.maintain_aspect_ratio(true),
|
||||
);
|
||||
});
|
||||
});
|
||||
ui.vertical(|ui| {
|
||||
let display_name = gossip_lib::names::display_name_from_person(&person);
|
||||
ui.heading(display_name);
|
||||
ui.label(RichText::new(gossip_lib::names::pubkey_short(&pubkey)));
|
||||
ui.add_space(10.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Pet name:");
|
||||
if app.editing_petname {
|
||||
let edit_color = app.theme.input_text_color();
|
||||
ui.add(TextEdit::singleline(&mut app.petname).text_color(edit_color));
|
||||
if ui.button("save").clicked() {
|
||||
let mut person = person.clone();
|
||||
person.petname = Some(app.petname.clone());
|
||||
if let Err(e) = GLOBALS.storage.write_person(&person, None) {
|
||||
GLOBALS.status_queue.write().write(format!("{}", e));
|
||||
}
|
||||
app.editing_petname = false;
|
||||
app.notes.cache_invalidate_person(&person.pubkey);
|
||||
}
|
||||
if ui.button("cancel").clicked() {
|
||||
app.editing_petname = false;
|
||||
}
|
||||
if ui.button("remove").clicked() {
|
||||
let mut person = person.clone();
|
||||
@ -98,23 +86,71 @@ fn content(app: &mut GossipUi, ctx: &Context, ui: &mut Ui, pubkey: PublicKey, pe
|
||||
if let Err(e) = GLOBALS.storage.write_person(&person, None) {
|
||||
GLOBALS.status_queue.write().write(format!("{}", e));
|
||||
}
|
||||
app.editing_petname = false;
|
||||
app.notes.cache_invalidate_person(&person.pubkey);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
ui.label(RichText::new("none").italics());
|
||||
if ui.button("add").clicked() {
|
||||
app.editing_petname = true;
|
||||
app.petname = "".to_owned();
|
||||
} else {
|
||||
match &person.petname {
|
||||
Some(pn) => {
|
||||
ui.label(pn);
|
||||
if ui.button("edit").clicked() {
|
||||
app.editing_petname = true;
|
||||
app.petname = pn.to_owned();
|
||||
}
|
||||
if ui.button("remove").clicked() {
|
||||
let mut person = person.clone();
|
||||
person.petname = None;
|
||||
if let Err(e) = GLOBALS.storage.write_person(&person, None)
|
||||
{
|
||||
GLOBALS.status_queue.write().write(format!("{}", e));
|
||||
}
|
||||
app.notes.cache_invalidate_person(&person.pubkey);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
ui.label(RichText::new("none").italics());
|
||||
if ui.button("add").clicked() {
|
||||
app.editing_petname = true;
|
||||
app.petname = "".to_owned();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(10.0);
|
||||
{
|
||||
let visuals = ui.visuals_mut();
|
||||
visuals.widgets.inactive.weak_bg_fill = app.theme.accent_color();
|
||||
visuals.widgets.inactive.fg_stroke.width = 1.0;
|
||||
visuals.widgets.inactive.fg_stroke.color =
|
||||
app.theme.get_style().visuals.extreme_bg_color;
|
||||
visuals.widgets.hovered.weak_bg_fill = app.theme.navigation_text_color();
|
||||
visuals.widgets.hovered.fg_stroke.color = app.theme.accent_color();
|
||||
visuals.widgets.inactive.fg_stroke.color =
|
||||
app.theme.get_style().visuals.extreme_bg_color;
|
||||
GossipUi::render_person_name_line(app, ui, &person, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if let Some(about) = person.about() {
|
||||
ui.add_space(10.0);
|
||||
ui.separator();
|
||||
ui.add_space(10.0);
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label(about);
|
||||
if ui.add(CopyButton {}).on_hover_text("Copy About").clicked() {
|
||||
ui.output_mut(|o| o.copied_text = about.to_owned());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
ui.add_space(12.0);
|
||||
ui.add_space(10.0);
|
||||
ui.separator();
|
||||
ui.add_space(10.0);
|
||||
|
||||
let npub = pubkey.as_bech32_string();
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
@ -143,18 +179,6 @@ fn content(app: &mut GossipUi, ctx: &Context, ui: &mut Ui, pubkey: PublicKey, pe
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(about) = person.about() {
|
||||
ui.label(RichText::new("About: ").strong());
|
||||
Frame::group(ui.style()).show(ui, |ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label(about);
|
||||
if ui.add(CopyButton {}).on_hover_text("Copy About").clicked() {
|
||||
ui.output_mut(|o| o.copied_text = about.to_owned());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(picture) = person.picture() {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.label(RichText::new("Picture: ").strong());
|
||||
@ -275,6 +299,8 @@ fn content(app: &mut GossipUi, ctx: &Context, ui: &mut Ui, pubkey: PublicKey, pe
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ui.add_space(10.0);
|
||||
}
|
||||
}
|
||||
if need_to_set_active_person && !app.setting_active_person {
|
||||
|
@ -15,6 +15,7 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr
|
||||
ui.add_space(10.0);
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| {
|
||||
ui.add_space(2.0);
|
||||
ui.heading(Page::RelaysActivityMonitor.name());
|
||||
ui.set_enabled(!is_editing);
|
||||
});
|
||||
|
@ -10,6 +10,7 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr
|
||||
let is_editing = app.relays.edit.is_some();
|
||||
ui.add_space(10.0);
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.add_space(2.0);
|
||||
ui.heading(Page::RelaysKnownNetwork.name());
|
||||
ui.set_enabled(!is_editing);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
|
||||
|
@ -11,6 +11,7 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr
|
||||
let is_editing = app.relays.edit.is_some();
|
||||
ui.add_space(10.0);
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.add_space(2.0);
|
||||
ui.heading(Page::RelaysMine.name());
|
||||
ui.set_enabled(!is_editing);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use super::{GossipUi, Page};
|
||||
use eframe::egui;
|
||||
use super::{widgets, GossipUi, Page};
|
||||
use eframe::{egui, epaint::PathShape};
|
||||
use egui::{Context, Ui};
|
||||
use egui_winit::egui::{vec2, Id, Rect, RichText};
|
||||
use gossip_lib::{comms::ToOverlordMessage, Relay, GLOBALS};
|
||||
@ -448,9 +448,12 @@ fn entry_dialog_step2(ui: &mut Ui, app: &mut GossipUi) {
|
||||
///
|
||||
pub(super) fn configure_list_btn(app: &mut GossipUi, ui: &mut Ui) {
|
||||
let (response, painter) = ui.allocate_painter(vec2(20.0, 20.0), egui::Sense::click());
|
||||
let response = response
|
||||
.on_hover_cursor(egui::CursorIcon::PointingHand)
|
||||
.on_hover_text("Configure List View");
|
||||
let response = response.on_hover_cursor(egui::CursorIcon::PointingHand);
|
||||
let response = if !app.relays.configure_list_menu_active {
|
||||
response.on_hover_text("Configure List View")
|
||||
} else {
|
||||
response
|
||||
};
|
||||
let btn_rect = response.rect;
|
||||
let color = if response.hovered() {
|
||||
app.theme.accent_color()
|
||||
@ -469,9 +472,8 @@ pub(super) fn configure_list_btn(app: &mut GossipUi, ui: &mut Ui) {
|
||||
app.relays.configure_list_menu_active ^= true;
|
||||
}
|
||||
|
||||
let mut seen_on_popup_position = response.rect.center_bottom();
|
||||
seen_on_popup_position.x -= 150.0;
|
||||
seen_on_popup_position.y += 18.0; // drop below the icon itself
|
||||
let button_center_bottom = response.rect.center_bottom();
|
||||
let seen_on_popup_position = button_center_bottom + vec2(-180.0, widgets::DROPDOWN_DISTANCE);
|
||||
|
||||
let id: Id = "configure-list-menu".into();
|
||||
let mut frame = egui::Frame::popup(ui.style());
|
||||
@ -483,14 +485,39 @@ pub(super) fn configure_list_btn(app: &mut GossipUi, ui: &mut Ui) {
|
||||
.constrain(true);
|
||||
if app.relays.configure_list_menu_active {
|
||||
let menuresp = area.show(ui.ctx(), |ui| {
|
||||
frame.fill = ui.visuals().extreme_bg_color;
|
||||
frame.inner_margin = egui::Margin::symmetric(20.0, 10.0);
|
||||
frame.fill = app.theme.accent_color();
|
||||
frame.stroke = egui::Stroke::NONE;
|
||||
// frame.shadow = egui::epaint::Shadow::NONE;
|
||||
frame.rounding = egui::Rounding::same(5.0);
|
||||
frame.inner_margin = egui::Margin::symmetric(20.0, 16.0);
|
||||
frame.show(ui, |ui| {
|
||||
let path = PathShape::convex_polygon(
|
||||
[
|
||||
button_center_bottom,
|
||||
button_center_bottom
|
||||
+ vec2(widgets::DROPDOWN_DISTANCE, widgets::DROPDOWN_DISTANCE),
|
||||
button_center_bottom
|
||||
+ vec2(-widgets::DROPDOWN_DISTANCE, widgets::DROPDOWN_DISTANCE),
|
||||
]
|
||||
.to_vec(),
|
||||
app.theme.accent_color(),
|
||||
egui::Stroke::NONE,
|
||||
);
|
||||
ui.painter().add(path);
|
||||
let size = ui.spacing().interact_size.y * egui::vec2(1.6, 0.8);
|
||||
crate::ui::components::switch_with_size(ui, &mut app.relays.show_details, size);
|
||||
ui.label("Show details");
|
||||
crate::ui::components::switch_with_size(ui, &mut app.relays.show_hidden, size);
|
||||
ui.label("Show hidden relays");
|
||||
|
||||
// since we are displaying over an accent color background, load that style
|
||||
*ui.style_mut() = app.theme.get_on_accent_style();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
crate::ui::components::switch_with_size(ui, &mut app.relays.show_details, size);
|
||||
ui.label("Show details");
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
ui.horizontal(|ui| {
|
||||
crate::ui::components::switch_with_size(ui, &mut app.relays.show_hidden, size);
|
||||
ui.label("Show hidden relays");
|
||||
});
|
||||
});
|
||||
});
|
||||
if menuresp.response.clicked_elsewhere() && !response.clicked() {
|
||||
|
@ -9,6 +9,7 @@ use gossip_lib::GLOBALS;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut Frame, ui: &mut Ui) {
|
||||
ui.add_space(10.0);
|
||||
ui.heading("Search notes and users");
|
||||
|
||||
ui.add_space(12.0);
|
||||
@ -81,7 +82,7 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut Frame, ui:
|
||||
ui.label(
|
||||
RichText::new(gossip_lib::names::pubkey_short(&person.pubkey)).weak(),
|
||||
);
|
||||
GossipUi::render_person_name_line(app, ui, person);
|
||||
GossipUi::render_person_name_line(app, ui, person, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -101,7 +102,7 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut Frame, ui:
|
||||
);
|
||||
|
||||
if let Ok(Some(person)) = GLOBALS.storage.read_person(&event.pubkey) {
|
||||
GossipUi::render_person_name_line(app, ui, &person);
|
||||
GossipUi::render_person_name_line(app, ui, &person, false);
|
||||
} else {
|
||||
ui.label(event.pubkey.as_bech32_string());
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ mod posting;
|
||||
mod ui;
|
||||
|
||||
pub(super) fn update(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Frame, ui: &mut Ui) {
|
||||
ui.add_space(10.0);
|
||||
ui.heading("Settings");
|
||||
|
||||
ui.with_layout(Layout::right_to_left(Align::Min), |ui| {
|
||||
|
@ -19,29 +19,26 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
|
||||
ui.add_space(20.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Theme:");
|
||||
if !app.theme.follow_os_dark_mode {
|
||||
if app.theme.dark_mode {
|
||||
if !app.settings.follow_os_dark_mode {
|
||||
if app.settings.dark_mode {
|
||||
if ui.add(Button::new("🌙 Dark")).on_hover_text("Switch to light mode").clicked() {
|
||||
app.theme.dark_mode = false;
|
||||
crate::ui::theme::apply_theme(&app.theme, ctx);
|
||||
app.settings.dark_mode = false;
|
||||
}
|
||||
} else {
|
||||
if ui.add(Button::new("☀ Light")).on_hover_text("Switch to dark mode").clicked() {
|
||||
app.theme.dark_mode = true;
|
||||
crate::ui::theme::apply_theme(&app.theme, ctx);
|
||||
app.settings.dark_mode = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
let theme_combo = egui::ComboBox::from_id_source("Theme");
|
||||
theme_combo.selected_text(app.theme.name()).show_ui(ui, |ui| {
|
||||
theme_combo.selected_text(&app.settings.theme_variant).show_ui(ui, |ui| {
|
||||
for theme_variant in ThemeVariant::all() {
|
||||
if ui.add(egui::widgets::SelectableLabel::new(*theme_variant == app.theme.variant, theme_variant.name())).clicked() {
|
||||
app.theme.variant = *theme_variant;
|
||||
crate::ui::theme::apply_theme(&app.theme, ctx);
|
||||
if ui.add(egui::widgets::SelectableLabel::new(theme_variant.name() == app.settings.theme_variant, theme_variant.name())).clicked() {
|
||||
app.settings.theme_variant = theme_variant.name().to_string();
|
||||
};
|
||||
}
|
||||
});
|
||||
ui.checkbox(&mut app.theme.follow_os_dark_mode, "Follow OS dark-mode").on_hover_text("Follow the operating system setting for dark-mode (requires app-restart to take effect)");
|
||||
ui.checkbox(&mut app.settings.follow_os_dark_mode, "Follow OS dark-mode").on_hover_text("Follow the operating system setting for dark-mode (requires app-restart to take effect)");
|
||||
});
|
||||
|
||||
ui.add_space(20.0);
|
||||
|
@ -1,595 +0,0 @@
|
||||
use super::{FeedProperties, NoteRenderData, ThemeDef};
|
||||
use crate::ui::HighlightType;
|
||||
use eframe::egui::style::{Selection, WidgetVisuals, Widgets};
|
||||
use eframe::egui::{FontDefinitions, Margin, RichText, Style, TextFormat, TextStyle, Visuals};
|
||||
use eframe::epaint::{ecolor, Color32, FontFamily, FontId, Rounding, Shadow, Stroke};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ClassicTheme {}
|
||||
|
||||
impl ThemeDef for ClassicTheme {
|
||||
fn name() -> &'static str {
|
||||
"Classic"
|
||||
}
|
||||
|
||||
fn accent_color(dark_mode: bool) -> Color32 {
|
||||
// not used within
|
||||
if dark_mode {
|
||||
Color32::from_rgb(116, 167, 204)
|
||||
} else {
|
||||
Color32::from_rgb(85, 122, 149)
|
||||
}
|
||||
}
|
||||
|
||||
fn accent_complementary_color(dark_mode: bool) -> Color32 {
|
||||
// not used within
|
||||
let mut hsva: ecolor::HsvaGamma = Self::accent_color(dark_mode).into();
|
||||
hsva.h = (hsva.h + 0.5) % 1.0;
|
||||
hsva.into()
|
||||
}
|
||||
|
||||
fn highlighted_note_bgcolor(dark_mode: bool) -> Color32 {
|
||||
// not used within
|
||||
if dark_mode {
|
||||
Color32::from_rgb(41, 34, 46)
|
||||
} else {
|
||||
Color32::from_rgb(255, 255, 237)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_style(dark_mode: bool) -> Style {
|
||||
let mut style = Style::default();
|
||||
|
||||
// /// `item_spacing` is inserted _after_ adding a widget, so to increase the spacing between
|
||||
// /// widgets `A` and `B` you need to change `item_spacing` before adding `A`.
|
||||
// pub item_spacing: Vec2,
|
||||
|
||||
// /// Horizontal and vertical margins within a window frame.
|
||||
// pub window_margin: Margin,
|
||||
|
||||
// /// Button size is text size plus this on each side
|
||||
// pub button_padding: Vec2,
|
||||
|
||||
// /// Horizontal and vertical margins within a menu frame.
|
||||
// pub menu_margin: Margin,
|
||||
|
||||
// /// Indent collapsing regions etc by this much.
|
||||
// pub indent: f32,
|
||||
|
||||
// /// Minimum size of a [`DragValue`], color picker button, and other small widgets.
|
||||
// /// `interact_size.y` is the default height of button, slider, etc.
|
||||
// /// Anything clickable should be (at least) this size.
|
||||
// pub interact_size: Vec2, // TODO(emilk): rename min_interact_size ?
|
||||
|
||||
// /// Default width of a [`Slider`].
|
||||
// pub slider_width: f32,
|
||||
|
||||
// /// Default (minimum) width of a [`ComboBox`](gossip_lib::ComboBox).
|
||||
// pub combo_width: f32,
|
||||
|
||||
// /// Default width of a [`TextEdit`].
|
||||
// pub text_edit_width: f32,
|
||||
|
||||
// /// Checkboxes, radio button and collapsing headers have an icon at the start.
|
||||
// /// This is the width/height of the outer part of this icon (e.g. the BOX of the checkbox).
|
||||
// pub icon_width: f32,
|
||||
|
||||
// /// Checkboxes, radio button and collapsing headers have an icon at the start.
|
||||
// /// This is the width/height of the inner part of this icon (e.g. the check of the checkbox).
|
||||
// pub icon_width_inner: f32,
|
||||
|
||||
// /// Checkboxes, radio button and collapsing headers have an icon at the start.
|
||||
// /// This is the spacing between the icon and the text
|
||||
// pub icon_spacing: f32,
|
||||
// /// Width of a tooltip (`on_hover_ui`, `on_hover_text` etc).
|
||||
// pub tooltip_width: f32,
|
||||
|
||||
// /// End indented regions with a horizontal line
|
||||
// pub indent_ends_with_horizontal_line: bool,
|
||||
|
||||
// /// Height of a combo-box before showing scroll bars.
|
||||
// pub combo_height: f32,
|
||||
|
||||
// pub scroll_bar_width: f32,
|
||||
|
||||
// /// Make sure the scroll handle is at least this big
|
||||
// pub scroll_handle_min_length: f32,
|
||||
|
||||
// /// Margin between contents and scroll bar.
|
||||
// pub scroll_bar_inner_margin: f32,
|
||||
|
||||
// /// Margin between scroll bar and the outer container (e.g. right of a vertical scroll bar).
|
||||
// pub scroll_bar_outer_margin: f32,
|
||||
|
||||
if dark_mode {
|
||||
style.visuals = Visuals {
|
||||
dark_mode: true,
|
||||
widgets: Widgets {
|
||||
noninteractive: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(27),
|
||||
bg_fill: Color32::from_white_alpha(8),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(72)), // separators, borders
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(190)), // normal text color
|
||||
rounding: Rounding::same(2.0),
|
||||
expansion: 0.0,
|
||||
},
|
||||
inactive: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(60), // button background
|
||||
bg_fill: Color32::from_white_alpha(8),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(72)), // separators, borders
|
||||
// The following is used for All buttons, any clickable text,
|
||||
// AND text inputs, whether they are inactive OR active. It's really
|
||||
// overloaded.
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(190)), // button text
|
||||
rounding: Rounding::same(2.0),
|
||||
expansion: 0.0,
|
||||
},
|
||||
hovered: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(70),
|
||||
bg_fill: Color32::from_gray(70),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(150)), // e.g. hover over window edge or button
|
||||
fg_stroke: Stroke::new(1.5, Color32::from_gray(240)),
|
||||
rounding: Rounding::same(3.0),
|
||||
expansion: 1.0,
|
||||
},
|
||||
active: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(55),
|
||||
bg_fill: Color32::from_gray(55),
|
||||
bg_stroke: Stroke::new(1.0, Color32::WHITE),
|
||||
fg_stroke: Stroke::new(2.0, Color32::WHITE),
|
||||
rounding: Rounding::same(2.0),
|
||||
expansion: 1.0,
|
||||
},
|
||||
open: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(27),
|
||||
bg_fill: Color32::from_gray(27),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)),
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(210)),
|
||||
rounding: Rounding::same(2.0),
|
||||
expansion: 0.0,
|
||||
},
|
||||
},
|
||||
|
||||
// Background colors
|
||||
window_fill: Color32::from_gray(0x24), // pulldown menus and tooltips
|
||||
panel_fill: Color32::from_gray(0x24), // panel backgrounds, even-table-rows
|
||||
faint_bg_color: Color32::from_gray(0x14), // odd-table-rows
|
||||
extreme_bg_color: Color32::from_gray(0), // text input background; scrollbar background
|
||||
code_bg_color: Color32::from_gray(64), // ???
|
||||
|
||||
// Foreground colors
|
||||
window_stroke: Stroke::new(1.0, Color32::from_gray(230)),
|
||||
override_text_color: None,
|
||||
warn_fg_color: Color32::from_rgb(255, 143, 0), // orange
|
||||
error_fg_color: Color32::from_rgb(255, 0, 0), // red
|
||||
hyperlink_color: Color32::from_rgb(0x73, 0x95, 0xae), // light blue?
|
||||
|
||||
selection: Selection {
|
||||
bg_fill: Color32::from_rgb(0x57, 0x4a, 0x40),
|
||||
stroke: Stroke::new(1.0, Color32::from_gray(230)),
|
||||
},
|
||||
|
||||
window_shadow: Shadow::big_dark(),
|
||||
popup_shadow: Shadow::small_dark(),
|
||||
|
||||
indent_has_left_vline: false,
|
||||
menu_rounding: Rounding::same(2.0),
|
||||
slider_trailing_fill: true,
|
||||
striped: true,
|
||||
window_rounding: Rounding::same(6.0),
|
||||
resize_corner_size: 12.0,
|
||||
text_cursor: Stroke::new(2.0, Color32::from_rgb(192, 222, 255)),
|
||||
text_cursor_preview: false,
|
||||
clip_rect_margin: 3.0, // should be at least half the size of the widest frame stroke + max WidgetVisuals::expansion
|
||||
button_frame: true,
|
||||
collapsing_header_frame: false,
|
||||
interact_cursor: None,
|
||||
image_loading_spinners: true,
|
||||
};
|
||||
} else {
|
||||
style.visuals = Visuals {
|
||||
dark_mode: false,
|
||||
widgets: Widgets {
|
||||
noninteractive: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(248),
|
||||
bg_fill: Color32::from_black_alpha(20),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(192)),
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(80)), // normal text color
|
||||
rounding: Rounding::same(2.0),
|
||||
expansion: 0.0,
|
||||
},
|
||||
inactive: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(230), // button background
|
||||
bg_fill: Color32::from_black_alpha(20),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(192)),
|
||||
// The following is used for All buttons, any clickable text,
|
||||
// AND text inputs, whether they are inactive OR active. It's really
|
||||
// overloaded.
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // button text
|
||||
rounding: Rounding::same(2.0),
|
||||
expansion: 0.0,
|
||||
},
|
||||
hovered: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(220),
|
||||
bg_fill: Color32::from_gray(220),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(105)), // e.g. hover over window edge or button
|
||||
fg_stroke: Stroke::new(1.5, Color32::BLACK),
|
||||
rounding: Rounding::same(3.0),
|
||||
expansion: 1.0,
|
||||
},
|
||||
active: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(165),
|
||||
bg_fill: Color32::from_gray(165),
|
||||
bg_stroke: Stroke::new(1.0, Color32::BLACK),
|
||||
fg_stroke: Stroke::new(2.0, Color32::BLACK),
|
||||
rounding: Rounding::same(2.0),
|
||||
expansion: 1.0,
|
||||
},
|
||||
open: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(220),
|
||||
bg_fill: Color32::from_gray(220),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(160)),
|
||||
fg_stroke: Stroke::new(1.0, Color32::BLACK),
|
||||
rounding: Rounding::same(2.0),
|
||||
expansion: 0.0,
|
||||
},
|
||||
},
|
||||
|
||||
// Background colors
|
||||
window_fill: Color32::from_gray(0xed), // pulldown menus and tooltips
|
||||
panel_fill: Color32::from_gray(0xed), // panel backgrounds, even-table-rows
|
||||
faint_bg_color: Color32::from_gray(0xf9), // odd-table-rows
|
||||
extreme_bg_color: Color32::from_gray(0xff), // text input background; scrollbar background
|
||||
code_bg_color: Color32::from_gray(230), // ???
|
||||
|
||||
// Foreground colors
|
||||
window_stroke: Stroke::new(1.0, Color32::from_rgb(0x5d, 0x5c, 0x61)), // DONE
|
||||
override_text_color: None,
|
||||
warn_fg_color: Color32::from_rgb(255, 100, 0), // slightly orange red. it's difficult to find a warning color that pops on bright background.
|
||||
error_fg_color: Color32::from_rgb(255, 0, 0), // red
|
||||
hyperlink_color: Color32::from_rgb(0x55, 0x7a, 0x95), // DONE
|
||||
|
||||
selection: Selection {
|
||||
bg_fill: Color32::from_rgb(0xb1, 0xa2, 0x96), // DONE
|
||||
stroke: Stroke::new(1.0, Color32::from_rgb(0x5d, 0x5c, 0x61)), // DONE
|
||||
},
|
||||
|
||||
window_shadow: Shadow::big_light(),
|
||||
popup_shadow: Shadow::small_light(),
|
||||
|
||||
indent_has_left_vline: false,
|
||||
menu_rounding: Rounding::same(2.0),
|
||||
slider_trailing_fill: true,
|
||||
striped: true,
|
||||
window_rounding: Rounding::same(6.0),
|
||||
resize_corner_size: 12.0,
|
||||
text_cursor: Stroke::new(2.0, Color32::from_rgb(0, 83, 125)),
|
||||
text_cursor_preview: false,
|
||||
clip_rect_margin: 3.0, // should be at least half the size of the widest frame stroke + max WidgetVisuals::expansion
|
||||
button_frame: true,
|
||||
collapsing_header_frame: false,
|
||||
interact_cursor: None,
|
||||
image_loading_spinners: true,
|
||||
};
|
||||
}
|
||||
style
|
||||
}
|
||||
|
||||
fn font_definitions() -> FontDefinitions {
|
||||
super::font_definitions() // use default gossip font definitions
|
||||
}
|
||||
|
||||
fn text_styles() -> BTreeMap<TextStyle, FontId> {
|
||||
let mut text_styles: BTreeMap<TextStyle, FontId> = BTreeMap::new();
|
||||
|
||||
text_styles.insert(
|
||||
TextStyle::Small,
|
||||
FontId {
|
||||
size: 10.75,
|
||||
family: FontFamily::Proportional,
|
||||
},
|
||||
);
|
||||
|
||||
text_styles.insert(
|
||||
TextStyle::Body,
|
||||
FontId {
|
||||
size: 12.5,
|
||||
family: FontFamily::Proportional,
|
||||
},
|
||||
);
|
||||
|
||||
text_styles.insert(
|
||||
TextStyle::Monospace,
|
||||
FontId {
|
||||
size: 12.5,
|
||||
family: FontFamily::Monospace,
|
||||
},
|
||||
);
|
||||
|
||||
text_styles.insert(
|
||||
TextStyle::Button,
|
||||
FontId {
|
||||
size: 12.5,
|
||||
family: FontFamily::Proportional,
|
||||
},
|
||||
);
|
||||
|
||||
text_styles.insert(
|
||||
TextStyle::Heading,
|
||||
FontId {
|
||||
size: 16.25,
|
||||
family: FontFamily::Proportional,
|
||||
},
|
||||
);
|
||||
|
||||
// for subject lines in notes
|
||||
text_styles.insert(
|
||||
TextStyle::Name("subject".into()),
|
||||
FontId {
|
||||
size: 15.0,
|
||||
family: FontFamily::Proportional,
|
||||
},
|
||||
);
|
||||
|
||||
text_styles
|
||||
}
|
||||
|
||||
fn highlight_text_format(highlight_type: HighlightType, dark_mode: bool) -> TextFormat {
|
||||
let main = if dark_mode {
|
||||
Color32::WHITE
|
||||
} else {
|
||||
Color32::BLACK
|
||||
};
|
||||
let grey = if dark_mode {
|
||||
Color32::from_gray(36)
|
||||
} else {
|
||||
Color32::LIGHT_GRAY
|
||||
};
|
||||
let green = if dark_mode {
|
||||
Color32::LIGHT_GREEN
|
||||
} else {
|
||||
Color32::DARK_GREEN
|
||||
};
|
||||
let red = if dark_mode {
|
||||
Color32::LIGHT_RED
|
||||
} else {
|
||||
Color32::DARK_RED
|
||||
};
|
||||
let purple = if dark_mode {
|
||||
Color32::from_rgb(0xA0, 0x40, 0xA0)
|
||||
} else {
|
||||
Color32::from_rgb(0x80, 0, 0x80)
|
||||
};
|
||||
|
||||
match highlight_type {
|
||||
HighlightType::Nothing => TextFormat {
|
||||
font_id: FontId::new(12.5, FontFamily::Proportional),
|
||||
color: main,
|
||||
..Default::default()
|
||||
},
|
||||
HighlightType::PublicKey => TextFormat {
|
||||
font_id: FontId::new(12.5, FontFamily::Monospace),
|
||||
background: grey,
|
||||
color: green,
|
||||
..Default::default()
|
||||
},
|
||||
HighlightType::Event => TextFormat {
|
||||
font_id: FontId::new(12.5, FontFamily::Monospace),
|
||||
background: grey,
|
||||
color: red,
|
||||
..Default::default()
|
||||
},
|
||||
HighlightType::Relay => TextFormat {
|
||||
font_id: FontId::new(12.5, FontFamily::Monospace),
|
||||
background: grey,
|
||||
color: purple,
|
||||
..Default::default()
|
||||
},
|
||||
HighlightType::Hyperlink => TextFormat {
|
||||
font_id: FontId::new(12.5, FontFamily::Proportional),
|
||||
color: {
|
||||
// This should match get_style() above for hyperlink color.
|
||||
if dark_mode {
|
||||
Color32::from_rgb(0x73, 0x95, 0xae)
|
||||
} else {
|
||||
Color32::from_rgb(0x55, 0x7a, 0x95)
|
||||
}
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn warning_marker_text_color(dark_mode: bool) -> eframe::egui::Color32 {
|
||||
if dark_mode {
|
||||
Color32::LIGHT_RED
|
||||
} else {
|
||||
Color32::DARK_RED
|
||||
}
|
||||
}
|
||||
|
||||
fn notice_marker_text_color(dark_mode: bool) -> eframe::egui::Color32 {
|
||||
if dark_mode {
|
||||
Color32::LIGHT_BLUE
|
||||
} else {
|
||||
Color32::DARK_BLUE
|
||||
}
|
||||
}
|
||||
|
||||
fn navigation_bg_fill(dark_mode: bool) -> eframe::egui::Color32 {
|
||||
if dark_mode {
|
||||
Color32::from_rgb(0x30, 0x6f, 0xc1)
|
||||
} else {
|
||||
Color32::from_rgb(0x55, 0x7a, 0x95)
|
||||
}
|
||||
}
|
||||
|
||||
fn navigation_text_color(_dark_mode: bool) -> eframe::egui::Color32 {
|
||||
//if dark_mode {
|
||||
Color32::from_gray(220)
|
||||
//} else {
|
||||
// Color32::from_gray(220)
|
||||
//}
|
||||
}
|
||||
|
||||
fn navigation_text_active_color(_dark_mode: bool) -> eframe::egui::Color32 {
|
||||
//if dark_mode {
|
||||
Color32::from_gray(0xf9)
|
||||
//} else {
|
||||
// Color32::from_gray(0xf9)
|
||||
//}
|
||||
}
|
||||
|
||||
fn navigation_text_hover_color(_dark_mode: bool) -> eframe::egui::Color32 {
|
||||
Color32::WHITE
|
||||
}
|
||||
|
||||
fn navigation_header_active_color(_dark_mode: bool) -> eframe::egui::Color32 {
|
||||
Color32::from_gray(0xaa)
|
||||
}
|
||||
|
||||
fn input_text_color(dark_mode: bool) -> eframe::egui::Color32 {
|
||||
if dark_mode {
|
||||
Color32::from_gray(190)
|
||||
} else {
|
||||
Color32::from_gray(60)
|
||||
}
|
||||
}
|
||||
|
||||
// feed styling
|
||||
fn feed_scroll_rounding(_feed: &FeedProperties) -> Rounding {
|
||||
Rounding::ZERO
|
||||
}
|
||||
fn feed_scroll_fill(dark_mode: bool, _feed: &FeedProperties) -> Color32 {
|
||||
if dark_mode {
|
||||
Color32::BLACK
|
||||
} else {
|
||||
Color32::WHITE
|
||||
}
|
||||
}
|
||||
fn feed_scroll_stroke(_dark_mode: bool, _feed: &FeedProperties) -> Stroke {
|
||||
Stroke::NONE
|
||||
}
|
||||
fn feed_post_separator_stroke(dark_mode: bool, _post: &NoteRenderData) -> Stroke {
|
||||
if dark_mode {
|
||||
Stroke::new(1.0, Color32::from_gray(72))
|
||||
} else {
|
||||
Stroke::new(1.0, Color32::from_gray(192))
|
||||
}
|
||||
}
|
||||
fn feed_post_outer_indent(_ui: &mut eframe::egui::Ui, _post: &NoteRenderData) {}
|
||||
fn feed_post_inner_indent(ui: &mut eframe::egui::Ui, post: &NoteRenderData) {
|
||||
if post.is_thread {
|
||||
let space = 100.0 * (10.0 - (1000.0 / (post.thread_position as f32 + 100.0)));
|
||||
ui.add_space(space);
|
||||
if post.thread_position > 0 {
|
||||
ui.label(
|
||||
RichText::new(format!("{}>", post.thread_position))
|
||||
.italics()
|
||||
.weak(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn feed_frame_inner_margin(_post: &NoteRenderData) -> Margin {
|
||||
Margin {
|
||||
left: 10.0,
|
||||
top: 4.0,
|
||||
right: 10.0,
|
||||
bottom: 4.0,
|
||||
}
|
||||
}
|
||||
fn feed_frame_outer_margin(_post: &NoteRenderData) -> Margin {
|
||||
Margin {
|
||||
left: 0.0,
|
||||
top: 5.0,
|
||||
right: 0.0,
|
||||
bottom: 0.0,
|
||||
}
|
||||
}
|
||||
fn feed_frame_rounding(_post: &NoteRenderData) -> Rounding {
|
||||
Rounding::default()
|
||||
}
|
||||
fn feed_frame_shadow(_dark_mode: bool, _post: &NoteRenderData) -> Shadow {
|
||||
Shadow::default()
|
||||
}
|
||||
fn feed_frame_fill(dark_mode: bool, post: &NoteRenderData) -> Color32 {
|
||||
if post.is_new {
|
||||
if dark_mode {
|
||||
Color32::from_rgb(60, 0, 0)
|
||||
} else {
|
||||
Color32::LIGHT_YELLOW
|
||||
}
|
||||
} else {
|
||||
if dark_mode {
|
||||
Color32::BLACK
|
||||
} else {
|
||||
Color32::WHITE
|
||||
}
|
||||
}
|
||||
}
|
||||
fn feed_frame_stroke(_dark_mode: bool, _post: &NoteRenderData) -> Stroke {
|
||||
Stroke::NONE
|
||||
}
|
||||
|
||||
fn repost_separator_before_stroke(dark_mode: bool, _post: &NoteRenderData) -> Stroke {
|
||||
if dark_mode {
|
||||
Stroke::new(1.0, Color32::from_gray(72))
|
||||
} else {
|
||||
Stroke::new(1.0, Color32::from_gray(192))
|
||||
}
|
||||
}
|
||||
|
||||
fn repost_space_above_separator_before(_post: &NoteRenderData) -> f32 {
|
||||
4.0
|
||||
}
|
||||
fn repost_space_below_separator_before(_post: &NoteRenderData) -> f32 {
|
||||
8.0
|
||||
}
|
||||
|
||||
fn repost_separator_after_stroke(dark_mode: bool, post: &NoteRenderData) -> Stroke {
|
||||
Self::repost_separator_before_stroke(dark_mode, post)
|
||||
}
|
||||
fn repost_space_above_separator_after(_post: &NoteRenderData) -> f32 {
|
||||
4.0
|
||||
}
|
||||
fn repost_space_below_separator_after(_post: &NoteRenderData) -> f32 {
|
||||
0.0
|
||||
}
|
||||
|
||||
fn repost_inner_margin(_post: &NoteRenderData) -> Margin {
|
||||
// Margin {
|
||||
// left: 10.0,
|
||||
// top: 4.0,
|
||||
// right: 10.0,
|
||||
// bottom: 4.0,
|
||||
// }
|
||||
Margin::same(0.0)
|
||||
}
|
||||
fn repost_outer_margin(_post: &NoteRenderData) -> Margin {
|
||||
// Margin {
|
||||
// left: -10.0,
|
||||
// top: -4.0,
|
||||
// right: -10.0,
|
||||
// bottom: -4.0,
|
||||
// }
|
||||
Margin::same(0.0)
|
||||
}
|
||||
fn repost_rounding(post: &NoteRenderData) -> Rounding {
|
||||
Self::feed_frame_rounding(post)
|
||||
}
|
||||
fn repost_shadow(_dark_mode: bool, _post: &NoteRenderData) -> Shadow {
|
||||
Shadow::NONE
|
||||
}
|
||||
fn repost_fill(_dark_mode: bool, _post: &NoteRenderData) -> Color32 {
|
||||
Color32::TRANSPARENT
|
||||
}
|
||||
fn repost_stroke(_dark_mode: bool, _post: &NoteRenderData) -> Stroke {
|
||||
Stroke::NONE
|
||||
}
|
||||
|
||||
fn round_image() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
@ -129,7 +129,7 @@ impl ThemeDef for DefaultTheme {
|
||||
weak_bg_fill: Color32::from_white_alpha(4),
|
||||
bg_fill: Color32::from_white_alpha(20),
|
||||
bg_stroke: Stroke::new(0.0, Self::accent_color(dark_mode)), // e.g. hover over window edge or button
|
||||
fg_stroke: Stroke::new(1.5, Self::accent_color(dark_mode)),
|
||||
fg_stroke: Stroke::new(1.5, Color32::from_white_alpha(240)),
|
||||
rounding: Rounding::same(3.0),
|
||||
expansion: 2.0,
|
||||
},
|
||||
@ -137,7 +137,7 @@ impl ThemeDef for DefaultTheme {
|
||||
weak_bg_fill: Color32::from_gray(55),
|
||||
bg_fill: Color32::from_gray(55),
|
||||
bg_stroke: Stroke::new(0.0, Self::accent_color(dark_mode)),
|
||||
fg_stroke: Stroke::new(2.0, Color32::from_gray(220)),
|
||||
fg_stroke: Stroke::new(2.0, Color32::from_white_alpha(10)),
|
||||
rounding: Rounding::same(2.0),
|
||||
expansion: 2.0,
|
||||
},
|
||||
@ -194,7 +194,7 @@ impl ThemeDef for DefaultTheme {
|
||||
noninteractive: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(248),
|
||||
bg_fill: Color32::from_black_alpha(20),
|
||||
bg_stroke: Stroke::new(2.0, Color32::from_white_alpha(5)),
|
||||
bg_stroke: Stroke::new(2.0, Color32::from_white_alpha(1)),
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(80)), // normal text color
|
||||
rounding: Rounding::same(2.0),
|
||||
expansion: 0.0,
|
||||
@ -214,7 +214,7 @@ impl ThemeDef for DefaultTheme {
|
||||
weak_bg_fill: Color32::from_black_alpha(10),
|
||||
bg_fill: Color32::from_black_alpha(10),
|
||||
bg_stroke: Stroke::new(0.0, Self::accent_color(dark_mode)), // e.g. hover over window edge or button
|
||||
fg_stroke: Stroke::new(1.5, Self::accent_color(dark_mode)),
|
||||
fg_stroke: Stroke::new(1.5, Color32::from_black_alpha(240)),
|
||||
rounding: Rounding::same(3.0),
|
||||
expansion: 2.0,
|
||||
},
|
||||
@ -222,7 +222,7 @@ impl ThemeDef for DefaultTheme {
|
||||
weak_bg_fill: Color32::from_gray(165),
|
||||
bg_fill: Color32::from_black_alpha(50),
|
||||
bg_stroke: Stroke::new(0.0, Self::accent_color(dark_mode)),
|
||||
fg_stroke: Stroke::new(2.0, Color32::from_gray(30)),
|
||||
fg_stroke: Stroke::new(2.0, Color32::from_black_alpha(40)),
|
||||
rounding: Rounding::same(2.0),
|
||||
expansion: 2.0,
|
||||
},
|
||||
@ -276,6 +276,37 @@ impl ThemeDef for DefaultTheme {
|
||||
style
|
||||
}
|
||||
|
||||
/// the style to use when displaying on-top of an accent-colored background
|
||||
fn get_on_accent_style(dark_mode: bool) -> Style {
|
||||
let mut style = Self::get_style(dark_mode);
|
||||
if dark_mode {
|
||||
style.visuals.widgets.noninteractive.fg_stroke.color = style.visuals.window_fill;
|
||||
style.visuals.widgets.inactive.bg_fill = Color32::from_black_alpha(20);
|
||||
style.visuals.widgets.inactive.fg_stroke =
|
||||
Stroke::new(0.0, style.visuals.panel_fill.gamma_multiply(0.6));
|
||||
style.visuals.widgets.active.bg_fill = Color32::from_black_alpha(20);
|
||||
style.visuals.widgets.active.fg_stroke.color = style.visuals.window_fill;
|
||||
style.visuals.widgets.hovered.bg_fill = Color32::from_white_alpha(2);
|
||||
style.visuals.widgets.hovered.fg_stroke.color =
|
||||
style.visuals.panel_fill.gamma_multiply(0.6);
|
||||
style.visuals.selection.bg_fill = Self::accent_color(dark_mode).gamma_multiply(1.2);
|
||||
style.visuals.selection.stroke = Stroke::new(0.0, style.visuals.window_fill);
|
||||
} else {
|
||||
style.visuals.widgets.noninteractive.fg_stroke.color = style.visuals.panel_fill;
|
||||
style.visuals.widgets.inactive.bg_fill = Color32::from_black_alpha(20);
|
||||
style.visuals.widgets.inactive.fg_stroke =
|
||||
Stroke::new(0.0, style.visuals.panel_fill.gamma_multiply(0.6));
|
||||
style.visuals.widgets.active.bg_fill = style.visuals.panel_fill.gamma_multiply(0.6);
|
||||
style.visuals.widgets.active.fg_stroke.color = style.visuals.window_fill;
|
||||
style.visuals.widgets.hovered.bg_fill = Color32::from_white_alpha(2);
|
||||
style.visuals.widgets.hovered.fg_stroke.color =
|
||||
style.visuals.panel_fill.gamma_multiply(0.6);
|
||||
style.visuals.selection.bg_fill = Self::accent_color(dark_mode).gamma_multiply(1.2);
|
||||
style.visuals.selection.stroke = Stroke::new(0.0, style.visuals.panel_fill);
|
||||
}
|
||||
style
|
||||
}
|
||||
|
||||
fn font_definitions() -> FontDefinitions {
|
||||
super::font_definitions() // use default gossip font definitions
|
||||
}
|
||||
@ -415,49 +446,54 @@ impl ThemeDef for DefaultTheme {
|
||||
fn notice_marker_text_color(dark_mode: bool) -> eframe::egui::Color32 {
|
||||
let mut hsva: ecolor::HsvaGamma = Self::accent_color(dark_mode).into();
|
||||
if dark_mode {
|
||||
hsva.v = (hsva.v + 0.2).min(1.0); // lighten
|
||||
hsva.v = (hsva.v - 0.2).min(1.0); // darken++
|
||||
} else {
|
||||
hsva.v = (hsva.v - 0.2).max(0.0); // darken
|
||||
hsva.v = (hsva.v - 0.1).max(0.0); // darken
|
||||
}
|
||||
hsva.into()
|
||||
}
|
||||
|
||||
fn navigation_bg_fill(dark_mode: bool) -> eframe::egui::Color32 {
|
||||
let mut hsva: ecolor::HsvaGamma = Self::accent_color(dark_mode).into();
|
||||
hsva.s *= 0.7;
|
||||
hsva.v = if dark_mode { 0.23 } else { 0.56 };
|
||||
let mut hsva: ecolor::HsvaGamma = Self::get_style(dark_mode).visuals.panel_fill.into();
|
||||
let delta = if dark_mode { 1.3 } else { 0.90 };
|
||||
hsva.v *= delta;
|
||||
hsva.into()
|
||||
}
|
||||
|
||||
fn navigation_text_deactivated_color(dark_mode: bool) -> eframe::egui::Color32 {
|
||||
if dark_mode {
|
||||
Color32::from_white_alpha(10)
|
||||
} else {
|
||||
Color32::from_black_alpha(100)
|
||||
}
|
||||
}
|
||||
|
||||
fn navigation_text_color(dark_mode: bool) -> eframe::egui::Color32 {
|
||||
let mut hsva: ecolor::HsvaGamma = Self::accent_color(dark_mode).into();
|
||||
hsva.s = 0.05;
|
||||
hsva.v = if dark_mode { 0.56 } else { 0.86 };
|
||||
hsva.into()
|
||||
if dark_mode {
|
||||
Color32::from_white_alpha(40)
|
||||
} else {
|
||||
Color32::from_black_alpha(140)
|
||||
}
|
||||
}
|
||||
|
||||
fn navigation_text_active_color(dark_mode: bool) -> eframe::egui::Color32 {
|
||||
let mut hsva: ecolor::HsvaGamma = Self::accent_color(dark_mode).into();
|
||||
hsva.s = 0.05;
|
||||
hsva.v = if dark_mode { 0.86 } else { 0.97 };
|
||||
hsva.into()
|
||||
if dark_mode {
|
||||
Color32::from_white_alpha(140)
|
||||
} else {
|
||||
Color32::from_black_alpha(200)
|
||||
}
|
||||
}
|
||||
|
||||
fn navigation_text_hover_color(dark_mode: bool) -> eframe::egui::Color32 {
|
||||
let mut hsva: ecolor::HsvaGamma = Self::accent_color(dark_mode).into();
|
||||
hsva.s = 0.05;
|
||||
hsva.v = 1.00;
|
||||
hsva.into()
|
||||
Self::accent_color(dark_mode)
|
||||
}
|
||||
|
||||
fn navigation_header_active_color(dark_mode: bool) -> eframe::egui::Color32 {
|
||||
let mut hsva: ecolor::HsvaGamma = Self::accent_color(false).into();
|
||||
if dark_mode {
|
||||
hsva.v = (hsva.v + 0.1).min(1.0); // lighten
|
||||
Color32::from_white_alpha(80)
|
||||
} else {
|
||||
hsva.v = (hsva.v - 0.2).max(0.0); // darken
|
||||
Color32::from_black_alpha(80)
|
||||
}
|
||||
hsva.into()
|
||||
}
|
||||
|
||||
fn input_text_color(dark_mode: bool) -> eframe::egui::Color32 {
|
||||
|
@ -4,20 +4,14 @@ use eframe::egui::{
|
||||
Color32, Context, FontData, FontDefinitions, FontTweak, Margin, Rounding, Stroke, Style,
|
||||
TextFormat, TextStyle, Ui,
|
||||
};
|
||||
use eframe::epaint::{FontFamily, FontId, Shadow};
|
||||
use eframe::epaint::{ecolor, FontFamily, FontId, Shadow};
|
||||
use gossip_lib::Settings;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
mod classic;
|
||||
pub use classic::ClassicTheme;
|
||||
|
||||
mod default;
|
||||
pub use default::DefaultTheme;
|
||||
|
||||
mod roundy;
|
||||
pub use roundy::RoundyTheme;
|
||||
|
||||
pub fn apply_theme(theme: &Theme, ctx: &Context) {
|
||||
ctx.set_style(theme.get_style());
|
||||
ctx.set_fonts(theme.font_definitions());
|
||||
@ -29,9 +23,7 @@ pub fn apply_theme(theme: &Theme, ctx: &Context) {
|
||||
// note: if we store anything inside the variants, we can't use macro_rules.
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ThemeVariant {
|
||||
Classic,
|
||||
Default,
|
||||
Roundy,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@ -45,9 +37,7 @@ impl Theme {
|
||||
pub fn from_settings(settings: &Settings) -> Theme {
|
||||
Theme {
|
||||
variant: match &*settings.theme_variant {
|
||||
"Classic" => ThemeVariant::Classic,
|
||||
"Default" => ThemeVariant::Default,
|
||||
"Roundy" => ThemeVariant::Roundy,
|
||||
_ => ThemeVariant::Default,
|
||||
},
|
||||
dark_mode: settings.dark_mode,
|
||||
@ -79,6 +69,7 @@ macro_rules! theme_dispatch {
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
#[allow(dead_code)]
|
||||
pub fn name(&self) -> &'static str {
|
||||
self.variant.name()
|
||||
}
|
||||
@ -109,6 +100,12 @@ macro_rules! theme_dispatch {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_on_accent_style(&self) -> Style {
|
||||
match self.variant {
|
||||
$( $variant => $class::get_on_accent_style(self.dark_mode), )+
|
||||
}
|
||||
}
|
||||
|
||||
pub fn font_definitions(&self) -> FontDefinitions {
|
||||
match self.variant {
|
||||
$( $variant => $class::font_definitions(), )+
|
||||
@ -148,6 +145,12 @@ macro_rules! theme_dispatch {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn navigation_text_deactivated_color(&self) -> Color32 {
|
||||
match self.variant {
|
||||
$( $variant => $class::navigation_text_deactivated_color(self.dark_mode), )+
|
||||
}
|
||||
}
|
||||
|
||||
pub fn navigation_text_color(&self) -> Color32 {
|
||||
match self.variant {
|
||||
$( $variant => $class::navigation_text_color(self.dark_mode), )+
|
||||
@ -331,17 +334,7 @@ macro_rules! theme_dispatch {
|
||||
}
|
||||
}
|
||||
|
||||
theme_dispatch!(
|
||||
ThemeVariant::Classic,
|
||||
ClassicTheme,
|
||||
"Classic",
|
||||
ThemeVariant::Default,
|
||||
DefaultTheme,
|
||||
"Default",
|
||||
ThemeVariant::Roundy,
|
||||
RoundyTheme,
|
||||
"Roundy"
|
||||
);
|
||||
theme_dispatch!(ThemeVariant::Default, DefaultTheme, "Default");
|
||||
|
||||
pub trait ThemeDef: Send + Sync {
|
||||
// User facing name
|
||||
@ -358,6 +351,8 @@ pub trait ThemeDef: Send + Sync {
|
||||
// These styles are used by egui by default for widgets if you don't override them
|
||||
// in place.
|
||||
fn get_style(dark_mode: bool) -> Style;
|
||||
/// the style to use when displaying on-top of an accent-colored background
|
||||
fn get_on_accent_style(dark_mode: bool) -> Style;
|
||||
|
||||
fn font_definitions() -> FontDefinitions;
|
||||
fn text_styles() -> BTreeMap<TextStyle, FontId>;
|
||||
@ -366,6 +361,7 @@ pub trait ThemeDef: Send + Sync {
|
||||
fn notice_marker_text_color(dark_mode: bool) -> eframe::egui::Color32;
|
||||
|
||||
fn navigation_bg_fill(dark_mode: bool) -> eframe::egui::Color32;
|
||||
fn navigation_text_deactivated_color(dark_mode: bool) -> eframe::egui::Color32;
|
||||
fn navigation_text_color(dark_mode: bool) -> eframe::egui::Color32;
|
||||
fn navigation_text_active_color(dark_mode: bool) -> eframe::egui::Color32;
|
||||
fn navigation_text_hover_color(dark_mode: bool) -> eframe::egui::Color32;
|
||||
@ -404,6 +400,12 @@ pub trait ThemeDef: Send + Sync {
|
||||
|
||||
// image rounding
|
||||
fn round_image() -> bool;
|
||||
|
||||
fn darken_color(color: Color32, factor: f32) -> Color32 {
|
||||
let mut hsva: ecolor::HsvaGamma = color.into();
|
||||
hsva.v = (hsva.v * factor).max(0.0).min(1.0);
|
||||
hsva.into()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn font_definitions() -> FontDefinitions {
|
||||
|
@ -1,618 +0,0 @@
|
||||
use super::{FeedProperties, NoteRenderData, ThemeDef};
|
||||
use crate::ui::HighlightType;
|
||||
use eframe::egui::style::{Selection, WidgetVisuals, Widgets};
|
||||
use eframe::egui::{FontDefinitions, Margin, Style, TextFormat, TextStyle, Visuals};
|
||||
use eframe::epaint::{ecolor, Color32, FontFamily, FontId, Rounding, Shadow, Stroke};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RoundyTheme {}
|
||||
|
||||
impl ThemeDef for RoundyTheme {
|
||||
fn name() -> &'static str {
|
||||
"Roundy"
|
||||
}
|
||||
|
||||
fn accent_color(dark_mode: bool) -> Color32 {
|
||||
// not used within
|
||||
if dark_mode {
|
||||
Color32::from_rgb(116, 167, 204)
|
||||
} else {
|
||||
Color32::from_rgb(85, 122, 149)
|
||||
}
|
||||
}
|
||||
|
||||
fn accent_complementary_color(dark_mode: bool) -> Color32 {
|
||||
// not used within
|
||||
let mut hsva: ecolor::HsvaGamma = Self::accent_color(dark_mode).into();
|
||||
hsva.h = (hsva.h + 0.5) % 1.0;
|
||||
hsva.into()
|
||||
}
|
||||
|
||||
fn highlighted_note_bgcolor(dark_mode: bool) -> Color32 {
|
||||
// not used within
|
||||
if dark_mode {
|
||||
Color32::from_rgb(41, 34, 46)
|
||||
} else {
|
||||
Color32::from_rgb(255, 255, 237)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_style(dark_mode: bool) -> Style {
|
||||
let mut style = Style::default();
|
||||
|
||||
// /// `item_spacing` is inserted _after_ adding a widget, so to increase the spacing between
|
||||
// /// widgets `A` and `B` you need to change `item_spacing` before adding `A`.
|
||||
// pub item_spacing: Vec2,
|
||||
|
||||
// /// Horizontal and vertical margins within a window frame.
|
||||
// pub window_margin: Margin,
|
||||
style.spacing.window_margin = Margin::symmetric(20.0, 20.0);
|
||||
|
||||
// /// Button size is text size plus this on each side
|
||||
// pub button_padding: Vec2,
|
||||
|
||||
// /// Horizontal and vertical margins within a menu frame.
|
||||
// pub menu_margin: Margin,
|
||||
style.spacing.menu_margin = Margin::symmetric(10.0, 5.0);
|
||||
|
||||
// /// Indent collapsing regions etc by this much.
|
||||
// pub indent: f32,
|
||||
|
||||
// /// Minimum size of a [`DragValue`], color picker button, and other small widgets.
|
||||
// /// `interact_size.y` is the default height of button, slider, etc.
|
||||
// /// Anything clickable should be (at least) this size.
|
||||
// pub interact_size: Vec2, // TODO(emilk): rename min_interact_size ?
|
||||
|
||||
// /// Default width of a [`Slider`].
|
||||
// pub slider_width: f32,
|
||||
|
||||
// /// Default (minimum) width of a [`ComboBox`](gossip_lib::ComboBox).
|
||||
// pub combo_width: f32,
|
||||
|
||||
// /// Default width of a [`TextEdit`].
|
||||
// pub text_edit_width: f32,
|
||||
|
||||
// /// Checkboxes, radio button and collapsing headers have an icon at the start.
|
||||
// /// This is the width/height of the outer part of this icon (e.g. the BOX of the checkbox).
|
||||
// pub icon_width: f32,
|
||||
|
||||
// /// Checkboxes, radio button and collapsing headers have an icon at the start.
|
||||
// /// This is the width/height of the inner part of this icon (e.g. the check of the checkbox).
|
||||
// pub icon_width_inner: f32,
|
||||
|
||||
// /// Checkboxes, radio button and collapsing headers have an icon at the start.
|
||||
// /// This is the spacing between the icon and the text
|
||||
// pub icon_spacing: f32,
|
||||
|
||||
// /// Width of a tooltip (`on_hover_ui`, `on_hover_text` etc).
|
||||
// pub tooltip_width: f32,
|
||||
|
||||
// /// End indented regions with a horizontal line
|
||||
// pub indent_ends_with_horizontal_line: bool,
|
||||
|
||||
// /// Height of a combo-box before showing scroll bars.
|
||||
// pub combo_height: f32,
|
||||
|
||||
// pub scroll_bar_width: f32,
|
||||
//style.spacing.scroll_bar_width = 15.0;
|
||||
|
||||
// /// Make sure the scroll handle is at least this big
|
||||
//style.spacing.scroll_handle_min_length = 40.0;
|
||||
|
||||
// /// Margin between contents and scroll bar.
|
||||
// pub scroll_bar_inner_margin: f32,
|
||||
|
||||
// /// Margin between scroll bar and the outer container (e.g. right of a vertical scroll bar).
|
||||
// pub scroll_bar_outer_margin: f32,
|
||||
|
||||
if dark_mode {
|
||||
// ---- dark mode ------------------------------------------------------------------------------------------
|
||||
style.visuals = Visuals {
|
||||
dark_mode: true,
|
||||
widgets: Widgets {
|
||||
noninteractive: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(27),
|
||||
bg_fill: Color32::from_white_alpha(8),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(72)), // separators, borders
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(190)), // normal text color
|
||||
rounding: Rounding::same(2.0),
|
||||
expansion: 0.0,
|
||||
},
|
||||
inactive: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(60), // button background
|
||||
bg_fill: Color32::from_white_alpha(8),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(72)), // separators, borders
|
||||
// The following is used for All buttons, any clickable text,
|
||||
// AND text inputs, whether they are inactive OR active. It's really
|
||||
// overloaded.
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(190)), // button text
|
||||
rounding: Rounding::same(2.0),
|
||||
expansion: 0.0,
|
||||
},
|
||||
hovered: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(70),
|
||||
bg_fill: Color32::from_gray(70),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(150)), // e.g. hover over window edge or button
|
||||
fg_stroke: Stroke::new(1.5, Color32::from_gray(240)),
|
||||
rounding: Rounding::same(3.0),
|
||||
expansion: 1.0,
|
||||
},
|
||||
active: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(55),
|
||||
bg_fill: Color32::from_gray(55),
|
||||
bg_stroke: Stroke::new(1.0, Color32::WHITE),
|
||||
fg_stroke: Stroke::new(2.0, Color32::WHITE),
|
||||
rounding: Rounding::same(2.0),
|
||||
expansion: 1.0,
|
||||
},
|
||||
open: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(27),
|
||||
bg_fill: Color32::from_gray(27),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(60)),
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(210)),
|
||||
rounding: Rounding::same(2.0),
|
||||
expansion: 0.0,
|
||||
},
|
||||
},
|
||||
|
||||
// Background colors
|
||||
window_fill: Color32::from_gray(30), // pulldown menus and tooltips
|
||||
panel_fill: Color32::from_gray(30), // panel backgrounds, even-table-rows
|
||||
faint_bg_color: Color32::from_gray(0x14), // odd-table-rows
|
||||
extreme_bg_color: Color32::from_gray(0), // text input background; scrollbar background
|
||||
code_bg_color: Color32::from_gray(64), // ???
|
||||
|
||||
// Foreground colors
|
||||
window_stroke: Stroke::new(1.0, Color32::from_gray(230)),
|
||||
override_text_color: None,
|
||||
warn_fg_color: Color32::from_rgb(255, 143, 0), // orange
|
||||
error_fg_color: Color32::from_rgb(255, 0, 0), // red
|
||||
hyperlink_color: Color32::from_rgb(0x73, 0x95, 0xae), // light blue?
|
||||
|
||||
selection: Selection {
|
||||
bg_fill: Color32::from_rgb(0x57, 0x4a, 0x40),
|
||||
stroke: Stroke::new(1.0, Color32::from_gray(230)),
|
||||
},
|
||||
|
||||
window_shadow: Shadow::big_dark(),
|
||||
popup_shadow: Shadow::small_dark(),
|
||||
|
||||
indent_has_left_vline: false,
|
||||
menu_rounding: Rounding::same(2.0),
|
||||
slider_trailing_fill: true,
|
||||
striped: true,
|
||||
window_rounding: Rounding::same(6.0),
|
||||
resize_corner_size: 12.0,
|
||||
text_cursor: Stroke::new(2.0, Color32::from_rgb(192, 222, 255)),
|
||||
text_cursor_preview: false,
|
||||
clip_rect_margin: 3.0, // should be at least half the size of the widest frame stroke + max WidgetVisuals::expansion
|
||||
button_frame: true,
|
||||
collapsing_header_frame: false,
|
||||
interact_cursor: None,
|
||||
image_loading_spinners: true,
|
||||
};
|
||||
} else {
|
||||
// ---- light mode -----------------------------------------------------------------------------------------
|
||||
style.visuals = Visuals {
|
||||
dark_mode: false,
|
||||
widgets: Widgets {
|
||||
noninteractive: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(248),
|
||||
bg_fill: Color32::from_black_alpha(20),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(192)),
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(80)), // normal text color
|
||||
rounding: Rounding::same(2.0),
|
||||
expansion: 0.0,
|
||||
},
|
||||
inactive: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(230), // button background
|
||||
bg_fill: Color32::from_black_alpha(20),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(192)),
|
||||
// The following is used for All buttons, any clickable text,
|
||||
// AND text inputs, whether they are inactive OR active. It's really
|
||||
// overloaded.
|
||||
fg_stroke: Stroke::new(1.0, Color32::from_gray(60)), // button text
|
||||
rounding: Rounding::same(2.0),
|
||||
expansion: 0.0,
|
||||
},
|
||||
hovered: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(220),
|
||||
bg_fill: Color32::from_gray(220),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(105)), // e.g. hover over window edge or button
|
||||
fg_stroke: Stroke::new(1.5, Color32::BLACK),
|
||||
rounding: Rounding::same(3.0),
|
||||
expansion: 1.0,
|
||||
},
|
||||
active: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(165),
|
||||
bg_fill: Color32::from_gray(165),
|
||||
bg_stroke: Stroke::new(1.0, Color32::BLACK),
|
||||
fg_stroke: Stroke::new(2.0, Color32::BLACK),
|
||||
rounding: Rounding::same(2.0),
|
||||
expansion: 1.0,
|
||||
},
|
||||
open: WidgetVisuals {
|
||||
weak_bg_fill: Color32::from_gray(220),
|
||||
bg_fill: Color32::from_gray(220),
|
||||
bg_stroke: Stroke::new(1.0, Color32::from_gray(160)),
|
||||
fg_stroke: Stroke::new(1.0, Color32::BLACK),
|
||||
rounding: Rounding::same(2.0),
|
||||
expansion: 0.0,
|
||||
},
|
||||
},
|
||||
|
||||
// Background colors
|
||||
window_fill: Color32::from_gray(0xec), // pulldown menus and tooltips
|
||||
panel_fill: Color32::from_gray(0xec), // panel backgrounds, even-table-rows
|
||||
faint_bg_color: Color32::from_gray(0xf9), // odd-table-rows
|
||||
extreme_bg_color: Color32::from_gray(0xff), // text input background; scrollbar background
|
||||
code_bg_color: Color32::from_gray(230), // ???
|
||||
|
||||
// Foreground colors
|
||||
window_stroke: Stroke::new(1.0, Color32::from_rgb(0x5d, 0x5c, 0x61)), // DONE
|
||||
override_text_color: None,
|
||||
warn_fg_color: Color32::from_rgb(255, 100, 0), // slightly orange red. it's difficult to find a warning color that pops on bright background.
|
||||
error_fg_color: Color32::from_rgb(255, 0, 0), // red
|
||||
hyperlink_color: Color32::from_rgb(0x55, 0x7a, 0x95), // DONE
|
||||
|
||||
selection: Selection {
|
||||
bg_fill: Color32::WHITE, // DONE
|
||||
stroke: Stroke::new(1.0, Color32::from_rgb(0x5d, 0x5c, 0x61)), // DONE
|
||||
},
|
||||
|
||||
window_shadow: Shadow::big_light(),
|
||||
popup_shadow: Shadow::small_light(),
|
||||
|
||||
indent_has_left_vline: false,
|
||||
menu_rounding: Rounding::same(2.0),
|
||||
slider_trailing_fill: true,
|
||||
striped: true,
|
||||
window_rounding: Rounding::same(6.0),
|
||||
resize_corner_size: 12.0,
|
||||
text_cursor: Stroke::new(2.0, Color32::from_rgb(0, 83, 125)),
|
||||
text_cursor_preview: false,
|
||||
clip_rect_margin: 3.0, // should be at least half the size of the widest frame stroke + max WidgetVisuals::expansion
|
||||
button_frame: true,
|
||||
collapsing_header_frame: false,
|
||||
interact_cursor: None,
|
||||
image_loading_spinners: true,
|
||||
};
|
||||
}
|
||||
style
|
||||
}
|
||||
|
||||
fn font_definitions() -> FontDefinitions {
|
||||
super::font_definitions() // use default gossip font definitions
|
||||
}
|
||||
|
||||
fn text_styles() -> BTreeMap<TextStyle, FontId> {
|
||||
let mut text_styles: BTreeMap<TextStyle, FontId> = BTreeMap::new();
|
||||
|
||||
text_styles.insert(
|
||||
TextStyle::Small,
|
||||
FontId {
|
||||
size: 10.75,
|
||||
family: FontFamily::Proportional,
|
||||
},
|
||||
);
|
||||
|
||||
text_styles.insert(
|
||||
TextStyle::Body,
|
||||
FontId {
|
||||
size: 12.5,
|
||||
family: FontFamily::Proportional,
|
||||
},
|
||||
);
|
||||
|
||||
text_styles.insert(
|
||||
TextStyle::Monospace,
|
||||
FontId {
|
||||
size: 12.5,
|
||||
family: FontFamily::Monospace,
|
||||
},
|
||||
);
|
||||
|
||||
text_styles.insert(
|
||||
TextStyle::Button,
|
||||
FontId {
|
||||
size: 12.5,
|
||||
family: FontFamily::Proportional,
|
||||
},
|
||||
);
|
||||
|
||||
text_styles.insert(
|
||||
TextStyle::Heading,
|
||||
FontId {
|
||||
size: 16.25,
|
||||
family: FontFamily::Proportional,
|
||||
},
|
||||
);
|
||||
|
||||
// for subject lines in notes
|
||||
text_styles.insert(
|
||||
TextStyle::Name("subject".into()),
|
||||
FontId {
|
||||
size: 15.0,
|
||||
family: FontFamily::Proportional,
|
||||
},
|
||||
);
|
||||
|
||||
text_styles
|
||||
}
|
||||
|
||||
fn highlight_text_format(highlight_type: HighlightType, dark_mode: bool) -> TextFormat {
|
||||
let main = if dark_mode {
|
||||
Color32::WHITE
|
||||
} else {
|
||||
Color32::BLACK
|
||||
};
|
||||
let grey = if dark_mode {
|
||||
Color32::from_gray(36)
|
||||
} else {
|
||||
Color32::LIGHT_GRAY
|
||||
};
|
||||
let green = if dark_mode {
|
||||
Color32::LIGHT_GREEN
|
||||
} else {
|
||||
Color32::DARK_GREEN
|
||||
};
|
||||
let red = if dark_mode {
|
||||
Color32::LIGHT_RED
|
||||
} else {
|
||||
Color32::DARK_RED
|
||||
};
|
||||
let purple = if dark_mode {
|
||||
Color32::from_rgb(0xA0, 0x40, 0xA0)
|
||||
} else {
|
||||
Color32::from_rgb(0x80, 0, 0x80)
|
||||
};
|
||||
|
||||
match highlight_type {
|
||||
HighlightType::Nothing => TextFormat {
|
||||
font_id: FontId::new(12.5, FontFamily::Proportional),
|
||||
color: main,
|
||||
..Default::default()
|
||||
},
|
||||
HighlightType::PublicKey => TextFormat {
|
||||
font_id: FontId::new(12.5, FontFamily::Monospace),
|
||||
background: grey,
|
||||
color: green,
|
||||
..Default::default()
|
||||
},
|
||||
HighlightType::Event => TextFormat {
|
||||
font_id: FontId::new(12.5, FontFamily::Monospace),
|
||||
background: grey,
|
||||
color: red,
|
||||
..Default::default()
|
||||
},
|
||||
HighlightType::Relay => TextFormat {
|
||||
font_id: FontId::new(12.5, FontFamily::Monospace),
|
||||
background: grey,
|
||||
color: purple,
|
||||
..Default::default()
|
||||
},
|
||||
HighlightType::Hyperlink => TextFormat {
|
||||
font_id: FontId::new(12.5, FontFamily::Proportional),
|
||||
color: {
|
||||
// This should match get_style() above for hyperlink color.
|
||||
if dark_mode {
|
||||
Color32::from_rgb(0x73, 0x95, 0xae)
|
||||
} else {
|
||||
Color32::from_rgb(0x55, 0x7a, 0x95)
|
||||
}
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn warning_marker_text_color(dark_mode: bool) -> eframe::egui::Color32 {
|
||||
if dark_mode {
|
||||
Color32::LIGHT_RED
|
||||
} else {
|
||||
Color32::DARK_RED
|
||||
}
|
||||
}
|
||||
|
||||
fn notice_marker_text_color(dark_mode: bool) -> eframe::egui::Color32 {
|
||||
if dark_mode {
|
||||
Color32::LIGHT_BLUE
|
||||
} else {
|
||||
Color32::DARK_BLUE
|
||||
}
|
||||
}
|
||||
|
||||
fn navigation_bg_fill(dark_mode: bool) -> eframe::egui::Color32 {
|
||||
if dark_mode {
|
||||
Color32::from_rgb(0x30, 0x6f, 0xc1)
|
||||
} else {
|
||||
Color32::from_rgb(0x55, 0x7a, 0x95)
|
||||
}
|
||||
}
|
||||
|
||||
fn navigation_text_color(_dark_mode: bool) -> eframe::egui::Color32 {
|
||||
//if dark_mode {
|
||||
Color32::from_gray(220)
|
||||
//} else {
|
||||
// Color32::from_gray(220)
|
||||
//}
|
||||
}
|
||||
|
||||
fn navigation_text_active_color(_dark_mode: bool) -> eframe::egui::Color32 {
|
||||
//if dark_mode {
|
||||
Color32::from_gray(0xf9)
|
||||
//} else {
|
||||
// Color32::from_gray(0xf9)
|
||||
//}
|
||||
}
|
||||
|
||||
fn navigation_text_hover_color(_dark_mode: bool) -> eframe::egui::Color32 {
|
||||
Color32::WHITE
|
||||
}
|
||||
|
||||
fn navigation_header_active_color(_dark_mode: bool) -> eframe::egui::Color32 {
|
||||
Color32::from_gray(0xaa)
|
||||
}
|
||||
|
||||
fn input_text_color(dark_mode: bool) -> eframe::egui::Color32 {
|
||||
if dark_mode {
|
||||
Color32::from_gray(190)
|
||||
} else {
|
||||
Color32::from_gray(60)
|
||||
}
|
||||
}
|
||||
|
||||
// feed styling
|
||||
fn feed_scroll_rounding(_feed: &FeedProperties) -> Rounding {
|
||||
Rounding::same(7.0)
|
||||
}
|
||||
fn feed_scroll_fill(_dark_mode: bool, _feed: &FeedProperties) -> Color32 {
|
||||
Color32::TRANSPARENT
|
||||
}
|
||||
fn feed_scroll_stroke(_dark_mode: bool, _feed: &FeedProperties) -> Stroke {
|
||||
Stroke::NONE
|
||||
}
|
||||
fn feed_post_separator_stroke(_dark_mode: bool, _post: &NoteRenderData) -> Stroke {
|
||||
Stroke::new(1.0, Color32::TRANSPARENT)
|
||||
}
|
||||
fn feed_post_outer_indent(ui: &mut eframe::egui::Ui, post: &NoteRenderData) {
|
||||
if post.is_thread {
|
||||
let space = 100.0 * (10.0 - (1000.0 / (post.thread_position as f32 + 100.0)));
|
||||
ui.add_space(space);
|
||||
}
|
||||
}
|
||||
fn feed_post_inner_indent(_ui: &mut eframe::egui::Ui, _post: &NoteRenderData) {}
|
||||
fn feed_frame_inner_margin(_post: &NoteRenderData) -> Margin {
|
||||
Margin {
|
||||
left: 10.0,
|
||||
right: 10.0,
|
||||
top: 10.0,
|
||||
bottom: 5.0,
|
||||
}
|
||||
}
|
||||
fn feed_frame_outer_margin(_post: &NoteRenderData) -> Margin {
|
||||
Margin::default()
|
||||
}
|
||||
fn feed_frame_rounding(post: &NoteRenderData) -> Rounding {
|
||||
if post.is_thread {
|
||||
let mut rounding = Rounding::ZERO;
|
||||
if post.is_first && post.thread_position == 0 {
|
||||
rounding.nw = 7.0;
|
||||
rounding.ne = 7.0;
|
||||
}
|
||||
rounding
|
||||
} else {
|
||||
Rounding::same(7.0)
|
||||
}
|
||||
}
|
||||
fn feed_frame_shadow(_dark_mode: bool, _post: &NoteRenderData) -> Shadow {
|
||||
Shadow::NONE
|
||||
}
|
||||
fn feed_frame_fill(dark_mode: bool, post: &NoteRenderData) -> Color32 {
|
||||
if post.is_new {
|
||||
if dark_mode {
|
||||
Color32::from_rgb(45, 45, 46)
|
||||
} else {
|
||||
Color32::from_rgb(0xFF, 0xFF, 0xFA)
|
||||
}
|
||||
} else {
|
||||
if dark_mode {
|
||||
Color32::from_rgb(36, 36, 37)
|
||||
} else {
|
||||
Color32::WHITE
|
||||
}
|
||||
}
|
||||
}
|
||||
fn feed_frame_stroke(dark_mode: bool, post: &NoteRenderData) -> Stroke {
|
||||
if post.is_main_event {
|
||||
if dark_mode {
|
||||
Stroke::new(1.0, Color32::from_rgb(64, 96, 64))
|
||||
} else {
|
||||
Stroke::new(1.0, Color32::from_rgb(96, 128, 96))
|
||||
}
|
||||
} else {
|
||||
if dark_mode {
|
||||
Stroke::new(1.0, Color32::from_gray(50))
|
||||
} else {
|
||||
Stroke::new(1.0, Color32::from_gray(0xCC))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn repost_separator_before_stroke(dark_mode: bool, post: &NoteRenderData) -> Stroke {
|
||||
if post.is_comment_mention {
|
||||
return Stroke::NONE;
|
||||
}
|
||||
|
||||
if dark_mode {
|
||||
Stroke::new(1.0, Color32::from_gray(72))
|
||||
} else {
|
||||
Stroke::new(1.0, Color32::from_gray(192))
|
||||
}
|
||||
}
|
||||
fn repost_space_above_separator_before(_post: &NoteRenderData) -> f32 {
|
||||
0.0
|
||||
}
|
||||
fn repost_space_below_separator_before(post: &NoteRenderData) -> f32 {
|
||||
if !post.is_comment_mention {
|
||||
8.0
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
fn repost_separator_after_stroke(_dark_mode: bool, _post: &NoteRenderData) -> Stroke {
|
||||
Stroke::NONE
|
||||
}
|
||||
fn repost_space_above_separator_after(_post: &NoteRenderData) -> f32 {
|
||||
0.0
|
||||
}
|
||||
fn repost_space_below_separator_after(_post: &NoteRenderData) -> f32 {
|
||||
0.0
|
||||
}
|
||||
|
||||
fn repost_inner_margin(post: &NoteRenderData) -> Margin {
|
||||
Margin {
|
||||
left: 0.0,
|
||||
top: if post.is_comment_mention { 6.0 } else { 0.0 },
|
||||
right: 10.0,
|
||||
bottom: if post.is_comment_mention { 7.0 } else { 0.0 },
|
||||
}
|
||||
}
|
||||
fn repost_outer_margin(post: &NoteRenderData) -> Margin {
|
||||
Margin {
|
||||
left: 0.0,
|
||||
top: if post.is_comment_mention { 10.0 } else { 4.0 },
|
||||
right: -10.0,
|
||||
bottom: if post.is_comment_mention { 6.0 } else { 0.0 },
|
||||
}
|
||||
}
|
||||
fn repost_rounding(post: &NoteRenderData) -> Rounding {
|
||||
Self::feed_frame_rounding(post)
|
||||
}
|
||||
fn repost_shadow(_dark_mode: bool, _post: &NoteRenderData) -> Shadow {
|
||||
Shadow::NONE
|
||||
}
|
||||
fn repost_fill(dark_mode: bool, post: &NoteRenderData) -> Color32 {
|
||||
if !post.is_comment_mention {
|
||||
return Color32::TRANSPARENT;
|
||||
}
|
||||
|
||||
let mut hsva: ecolor::HsvaGamma = Self::feed_frame_fill(dark_mode, post).into();
|
||||
if dark_mode {
|
||||
hsva.v = (hsva.v + 0.05).min(1.0); // lighten
|
||||
} else {
|
||||
hsva.v = (hsva.v - 0.05).max(0.0); // darken
|
||||
}
|
||||
let color: Color32 = hsva.into();
|
||||
color
|
||||
}
|
||||
fn repost_stroke(_dark_mode: bool, _post: &NoteRenderData) -> Stroke {
|
||||
Stroke::NONE
|
||||
}
|
||||
|
||||
fn round_image() -> bool {
|
||||
true
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ mod relay_entry;
|
||||
pub use relay_entry::{RelayEntry, RelayEntryView};
|
||||
|
||||
use super::GossipUi;
|
||||
pub const DROPDOWN_DISTANCE: f32 = 10.0;
|
||||
|
||||
// pub fn break_anywhere_label(ui: &mut Ui, text: impl Into<WidgetText>) {
|
||||
// let mut job = text.into().into_text_job(
|
||||
|
@ -52,7 +52,6 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr
|
||||
.to_overlord
|
||||
.send(ToOverlordMessage::UpdateMetadata(*pk));
|
||||
// then remember we did so we don't keep doing it over and over again
|
||||
tracing::error!("DEBUGGING: fetching metadata for {}", pk.as_hex_string());
|
||||
app.wizard_state
|
||||
.followed_getting_metadata
|
||||
.insert(pk.to_owned());
|
||||
@ -134,25 +133,35 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr
|
||||
ui.label(" • Profile (nprofile1..)");
|
||||
ui.label(" • DNS ID (user@domain)");
|
||||
|
||||
ui.add_space(20.0);
|
||||
let mut label = RichText::new(" > Publish and Finish");
|
||||
if app.wizard_state.new_user {
|
||||
label = label.color(app.theme.accent_color());
|
||||
}
|
||||
if ui.button(label).clicked() {
|
||||
let _ = GLOBALS.to_overlord.send(ToOverlordMessage::PushFollow);
|
||||
if app.wizard_state.has_private_key {
|
||||
ui.add_space(20.0);
|
||||
let mut label = RichText::new(" > Publish and Finish");
|
||||
if app.wizard_state.new_user {
|
||||
label = label.color(app.theme.accent_color());
|
||||
}
|
||||
if ui.button(label).clicked() {
|
||||
let _ = GLOBALS.to_overlord.send(ToOverlordMessage::PushFollow);
|
||||
|
||||
let _ = GLOBALS.storage.write_wizard_complete(true, None);
|
||||
app.page = Page::Feed(FeedKind::Followed(false));
|
||||
}
|
||||
let _ = GLOBALS.storage.write_wizard_complete(true, None);
|
||||
app.page = Page::Feed(FeedKind::Followed(false));
|
||||
}
|
||||
|
||||
ui.add_space(20.0);
|
||||
let mut label = RichText::new(" > Finish without publishing");
|
||||
if !app.wizard_state.new_user {
|
||||
ui.add_space(20.0);
|
||||
let mut label = RichText::new(" > Finish without publishing");
|
||||
if !app.wizard_state.new_user {
|
||||
label = label.color(app.theme.accent_color());
|
||||
}
|
||||
if ui.button(label).clicked() {
|
||||
let _ = GLOBALS.storage.write_wizard_complete(true, None);
|
||||
app.page = Page::Feed(FeedKind::Followed(false));
|
||||
}
|
||||
} else {
|
||||
ui.add_space(20.0);
|
||||
let mut label = RichText::new(" > Finish");
|
||||
label = label.color(app.theme.accent_color());
|
||||
}
|
||||
if ui.button(label).clicked() {
|
||||
let _ = GLOBALS.storage.write_wizard_complete(true, None);
|
||||
app.page = Page::Feed(FeedKind::Followed(false));
|
||||
if ui.button(label).clicked() {
|
||||
let _ = GLOBALS.storage.write_wizard_complete(true, None);
|
||||
app.page = Page::Feed(FeedKind::Followed(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -226,47 +226,34 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr
|
||||
}
|
||||
|
||||
if !need_more {
|
||||
ui.add_space(20.0);
|
||||
let mut label = RichText::new(" > Publish and Continue");
|
||||
if app.wizard_state.new_user {
|
||||
label = label.color(app.theme.accent_color());
|
||||
}
|
||||
if ui.button(label).clicked() {
|
||||
let _ = GLOBALS
|
||||
.to_overlord
|
||||
.send(ToOverlordMessage::AdvertiseRelayList);
|
||||
app.page = Page::Wizard(WizardPage::SetupMetadata);
|
||||
}
|
||||
if app.wizard_state.has_private_key {
|
||||
ui.add_space(20.0);
|
||||
let mut label = RichText::new(" > Publish and Continue");
|
||||
if app.wizard_state.new_user {
|
||||
label = label.color(app.theme.accent_color());
|
||||
}
|
||||
if ui.button(label).clicked() {
|
||||
let _ = GLOBALS
|
||||
.to_overlord
|
||||
.send(ToOverlordMessage::AdvertiseRelayList);
|
||||
app.page = Page::Wizard(WizardPage::SetupMetadata);
|
||||
}
|
||||
|
||||
ui.add_space(20.0);
|
||||
let mut label = RichText::new(" > Continue without publishing");
|
||||
if !app.wizard_state.new_user {
|
||||
ui.add_space(20.0);
|
||||
let mut label = RichText::new(" > Continue without publishing");
|
||||
if !app.wizard_state.new_user {
|
||||
label = label.color(app.theme.accent_color());
|
||||
}
|
||||
if ui.button(label).clicked() {
|
||||
app.page = Page::Wizard(WizardPage::SetupMetadata);
|
||||
};
|
||||
} else {
|
||||
ui.add_space(20.0);
|
||||
let mut label = RichText::new(" > Continue");
|
||||
label = label.color(app.theme.accent_color());
|
||||
if ui.button(label).clicked() {
|
||||
app.page = Page::Wizard(WizardPage::SetupMetadata);
|
||||
};
|
||||
}
|
||||
if ui.button(label).clicked() {
|
||||
app.page = Page::Wizard(WizardPage::SetupMetadata);
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
[only if privatekey]
|
||||
|
||||
Please select several relays that you will publish your notes to. We recommend at
|
||||
least 2, but no more than 10.
|
||||
|
||||
Enter a Relay URL: _____________________ [ADD]
|
||||
|
||||
You may also pick from this list of popular relays, however be aware that this
|
||||
list may go out of date rapidly.
|
||||
---
|
||||
---
|
||||
---
|
||||
|
||||
( Continue ) ( 40 )
|
||||
( Go Back) ( 1 or 20, dep if they are new or not, which we have to save in a UI var )
|
||||
( Exit this Wizard)
|
||||
|
||||
(You can change your relays by visiting the RELAYS page)
|
||||
|
||||
*/
|
||||
}
|
||||
|
@ -6,7 +6,12 @@ use gossip_lib::GLOBALS;
|
||||
use tokio::task;
|
||||
|
||||
pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
|
||||
ui.heading("Delegatee");
|
||||
ui.add_space(10.0);
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
// ui.add_space(2.0);
|
||||
ui.heading("Delegatee");
|
||||
});
|
||||
ui.add_space(10.0);
|
||||
ui.label("If NIP-26 Delegation is set, I will post on behalf of the delegator");
|
||||
ui.add_space(24.0);
|
||||
|
||||
|
@ -14,8 +14,12 @@ lazy_static! {
|
||||
}
|
||||
|
||||
pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
|
||||
ui.add_space(24.0);
|
||||
|
||||
ui.add_space(10.0);
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
// ui.add_space(2.0);
|
||||
ui.heading("My profile");
|
||||
});
|
||||
ui.add_space(10.0);
|
||||
let public_key = match GLOBALS.signer.public_key() {
|
||||
Some(pk) => pk,
|
||||
None => {
|
||||
|
@ -14,7 +14,7 @@ mod metadata;
|
||||
pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
|
||||
if app.page == Page::YourKeys {
|
||||
ui.add_space(10.0);
|
||||
ui.heading("Your Keys");
|
||||
ui.heading("My Keys");
|
||||
|
||||
ui.add_space(10.0);
|
||||
ui.separator();
|
||||
|
@ -6,11 +6,4 @@ fn main() {
|
||||
.unwrap();
|
||||
let git_hash = String::from_utf8(output.stdout).unwrap();
|
||||
println!("cargo:rustc-env=GIT_HASH={git_hash}");
|
||||
|
||||
// link to bundled libraries
|
||||
#[cfg(target_os = "macos")]
|
||||
println!("cargo:rustc-link-arg=-Wl,-rpath,@loader_path");
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
println!("cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN");
|
||||
}
|
||||
|
@ -112,7 +112,8 @@ impl DmChannel {
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct DmChannelData {
|
||||
pub dm_channel: DmChannel,
|
||||
pub latest_message: Unixtime,
|
||||
pub latest_message_created_at: Unixtime,
|
||||
pub latest_message_content: String,
|
||||
pub message_count: usize,
|
||||
pub unread_message_count: usize,
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ pub enum ErrorKind {
|
||||
SliceError(std::array::TryFromSliceError),
|
||||
Speedy(speedy::Error),
|
||||
Svg(usvg::Error),
|
||||
TagNotIndexed(String),
|
||||
Timeout(tokio::time::error::Elapsed),
|
||||
UnknownCommand(String),
|
||||
UrlHasEmptyHostname,
|
||||
@ -104,6 +105,7 @@ impl std::fmt::Display for Error {
|
||||
SliceError(e) => write!(f, "Slice: {e}"),
|
||||
Speedy(e) => write!(f, "Speedy: {e}"),
|
||||
Svg(e) => write!(f, "SVG: {e}"),
|
||||
TagNotIndexed(s) => write!(f, "Tag not indexed: {s}"),
|
||||
Timeout(e) => write!(f, "Timeout: {e}"),
|
||||
UnknownCommand(s) => write!(f, "Unknown command: {s}"),
|
||||
UrlHasEmptyHostname => write!(f, "URL has empty hostname"),
|
||||
|
@ -2,7 +2,7 @@ use crate::comms::{ToMinionMessage, ToMinionPayload, ToMinionPayloadDetail, ToOv
|
||||
use crate::dm_channel::DmChannel;
|
||||
use crate::error::Error;
|
||||
use crate::globals::GLOBALS;
|
||||
use nostr_types::{EventDelegation, EventKind, Id, PublicKey, RelayUrl, Unixtime};
|
||||
use nostr_types::{Event, EventKind, Id, PublicKey, PublicKeyHex, RelayUrl, Unixtime};
|
||||
use parking_lot::RwLock;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
@ -310,6 +310,9 @@ impl Feed {
|
||||
|
||||
let since = now - Duration::from_secs(GLOBALS.storage.read_setting_feed_chunk());
|
||||
|
||||
// FIXME we don't include delegated events. We should look for all events
|
||||
// delegated to people we follow and include those in the feed too.
|
||||
|
||||
let followed_events: Vec<Id> = GLOBALS
|
||||
.storage
|
||||
.find_events(
|
||||
@ -353,43 +356,48 @@ impl Feed {
|
||||
let since =
|
||||
now - Duration::from_secs(GLOBALS.storage.read_setting_replies_chunk());
|
||||
|
||||
let my_pubkeyhex: PublicKeyHex = my_pubkey.into();
|
||||
|
||||
let inbox_events: Vec<Id> = GLOBALS
|
||||
.storage
|
||||
.read_events_referencing_person(&my_pubkey, since, |e| {
|
||||
if e.created_at > now {
|
||||
return false;
|
||||
} // no future events
|
||||
if dismissed.contains(&e.id) {
|
||||
return false;
|
||||
} // not dismissed
|
||||
//if e.pubkey == my_pubkey {
|
||||
// return false;
|
||||
//} // not self-authored
|
||||
|
||||
// Always include gift wrap and DMs
|
||||
if e.kind == EventKind::GiftWrap
|
||||
|| e.kind == EventKind::EncryptedDirectMessage
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Include if it directly replies to one of my events
|
||||
if let Some((id, _)) = e.replies_to() {
|
||||
if my_event_ids.contains(&id) {
|
||||
.find_tagged_events(
|
||||
"p",
|
||||
Some(my_pubkeyhex.as_str()),
|
||||
|e| {
|
||||
if e.created_at < since || e.created_at > now {
|
||||
return false;
|
||||
}
|
||||
if !kinds_with_dms.contains(&e.kind) {
|
||||
return false;
|
||||
}
|
||||
if dismissed.contains(&e.id) {
|
||||
return false;
|
||||
}
|
||||
if e.kind == EventKind::GiftWrap
|
||||
|| e.kind == EventKind::EncryptedDirectMessage
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if indirect {
|
||||
// Include if it tags me
|
||||
e.people().iter().any(|(p, _, _)| *p == my_pubkey.into())
|
||||
} else {
|
||||
// Include if it directly references me in the content
|
||||
e.people_referenced_in_content()
|
||||
.iter()
|
||||
.any(|p| *p == my_pubkey)
|
||||
}
|
||||
})?
|
||||
// Include if it directly replies to one of my events
|
||||
if let Some((id, _)) = e.replies_to() {
|
||||
if my_event_ids.contains(&id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if indirect {
|
||||
// Include if it tags me
|
||||
e.people().iter().any(|(p, _, _)| *p == my_pubkey.into())
|
||||
} else {
|
||||
// Include if it directly references me in the content
|
||||
e.people_referenced_in_content()
|
||||
.iter()
|
||||
.any(|p| *p == my_pubkey)
|
||||
}
|
||||
},
|
||||
true,
|
||||
)?
|
||||
.iter()
|
||||
.map(|e| e.id)
|
||||
.collect();
|
||||
@ -412,32 +420,41 @@ impl Feed {
|
||||
let since =
|
||||
now - Duration::from_secs(GLOBALS.storage.read_setting_person_feed_chunk());
|
||||
|
||||
let events: Vec<Id> = GLOBALS
|
||||
let pphex: PublicKeyHex = person_pubkey.into();
|
||||
|
||||
let filter = |e: &Event| {
|
||||
if dismissed.contains(&e.id) {
|
||||
return false;
|
||||
}
|
||||
if !kinds_without_dms.contains(&e.kind) {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
};
|
||||
|
||||
let mut events: Vec<Event> = GLOBALS
|
||||
.storage
|
||||
.find_events(
|
||||
&kinds_without_dms,
|
||||
&[], // any person (due to delegation condition) // FIXME
|
||||
&[person_pubkey],
|
||||
Some(since),
|
||||
|e| {
|
||||
if dismissed.contains(&e.id) {
|
||||
return false;
|
||||
} // not dismissed
|
||||
if e.pubkey == person_pubkey {
|
||||
true
|
||||
} else {
|
||||
if let EventDelegation::DelegatedBy(pk) = e.delegation() {
|
||||
pk == person_pubkey
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
},
|
||||
true,
|
||||
filter,
|
||||
false,
|
||||
)?
|
||||
.iter()
|
||||
.map(|e| e.id)
|
||||
.chain(
|
||||
GLOBALS
|
||||
.storage
|
||||
.find_tagged_events("delegation", Some(pphex.as_str()), filter, false)?
|
||||
.iter(),
|
||||
)
|
||||
.map(|e| e.to_owned())
|
||||
.collect();
|
||||
|
||||
events.sort_by(|a, b| b.created_at.cmp(&a.created_at).then(b.id.cmp(&a.id)));
|
||||
|
||||
let events: Vec<Id> = events.iter().map(|e| e.id).collect();
|
||||
|
||||
*self.person_feed.write() = events;
|
||||
}
|
||||
FeedKind::DmChat(channel) => {
|
||||
|
@ -48,6 +48,13 @@ impl Signer {
|
||||
.write("Ignored setting of public key (private key supercedes)".to_string());
|
||||
} else {
|
||||
*self.public.write() = Some(pk);
|
||||
|
||||
// Reubild the event tag index, since the 'p' tags it need to index just changed.
|
||||
task::spawn(async move {
|
||||
if let Err(e) = GLOBALS.storage.rebuild_event_tags_index(None) {
|
||||
tracing::error!("{}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,6 +78,13 @@ impl Signer {
|
||||
} else {
|
||||
*self.encrypted.write() = Some(epk);
|
||||
}
|
||||
|
||||
// Reubild the event tag index, since the 'p' tags it need to index just changed.
|
||||
task::spawn(async move {
|
||||
if let Err(e) = GLOBALS.storage.rebuild_event_tags_index(None) {
|
||||
tracing::error!("{}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn set_private_key(&self, pk: PrivateKey, pass: &str) -> Result<(), Error> {
|
||||
@ -78,6 +92,14 @@ impl Signer {
|
||||
Some(pk.export_encrypted(pass, GLOBALS.storage.read_setting_log_n())?);
|
||||
*self.public.write() = Some(pk.public_key());
|
||||
*self.private.write() = Some(pk);
|
||||
|
||||
// Reubild the event tag index, since the 'p' tags it need to index just changed.
|
||||
task::spawn(async move {
|
||||
if let Err(e) = GLOBALS.storage.rebuild_event_tags_index(None) {
|
||||
tracing::error!("{}", e);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -168,6 +190,11 @@ impl Signer {
|
||||
if let Err(e) = GLOBALS.signer.save().await {
|
||||
tracing::error!("{}", e);
|
||||
}
|
||||
|
||||
// Reubild the event tag index, since the 'p' tags it need to index just changed.
|
||||
if let Err(e) = GLOBALS.storage.rebuild_event_tags_index(None) {
|
||||
tracing::error!("{}", e);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
|
@ -1,163 +0,0 @@
|
||||
use crate::error::{Error, ErrorKind};
|
||||
use crate::globals::GLOBALS;
|
||||
use crate::storage::{RawDatabase, Storage};
|
||||
use heed::{types::UnalignedSlice, DatabaseFlags, RwTxn};
|
||||
use nostr_types::{Event, EventKind, Id, PublicKey, Unixtime};
|
||||
use speedy::Readable;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashSet;
|
||||
use std::ops::Bound;
|
||||
use std::sync::Mutex;
|
||||
|
||||
// PublicKey:ReverseUnixtime -> Id
|
||||
// (pubkey is referenced by the event somehow)
|
||||
// (only feed-displayable events are included)
|
||||
// (dup keys, so multiple Ids per key)
|
||||
// NOTE: this may be far too much data. Maybe we should only build this for the
|
||||
// user's pubkey as their inbox.
|
||||
|
||||
static EVENT_REFERENCES_PERSON1_DB_CREATE_LOCK: Mutex<()> = Mutex::new(());
|
||||
static mut EVENT_REFERENCES_PERSON1_DB: Option<RawDatabase> = None;
|
||||
|
||||
impl Storage {
|
||||
pub(super) fn db_event_references_person1(&self) -> Result<RawDatabase, Error> {
|
||||
unsafe {
|
||||
if let Some(db) = EVENT_REFERENCES_PERSON1_DB {
|
||||
Ok(db)
|
||||
} else {
|
||||
// Lock. This drops when anything returns.
|
||||
let _lock = EVENT_REFERENCES_PERSON1_DB_CREATE_LOCK.lock();
|
||||
|
||||
// In case of a race, check again
|
||||
if let Some(db) = EVENT_REFERENCES_PERSON1_DB {
|
||||
return Ok(db);
|
||||
}
|
||||
|
||||
// Create it. We know that nobody else is doing this and that
|
||||
// it cannot happen twice.
|
||||
let mut txn = self.env.write_txn()?;
|
||||
let db = self
|
||||
.env
|
||||
.database_options()
|
||||
.types::<UnalignedSlice<u8>, UnalignedSlice<u8>>()
|
||||
.flags(DatabaseFlags::DUP_SORT | DatabaseFlags::DUP_FIXED)
|
||||
.name("event_references_person")
|
||||
.create(&mut txn)?;
|
||||
txn.commit()?;
|
||||
EVENT_REFERENCES_PERSON1_DB = Some(db);
|
||||
Ok(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn write_event_references_person1<'a>(
|
||||
&'a self,
|
||||
event: &Event,
|
||||
rw_txn: Option<&mut RwTxn<'a>>,
|
||||
) -> Result<(), Error> {
|
||||
let f = |txn: &mut RwTxn<'a>| -> Result<(), Error> {
|
||||
let mut event = event;
|
||||
|
||||
// If giftwrap, index the inner rumor instead
|
||||
let mut rumor_event: Event;
|
||||
if event.kind == EventKind::GiftWrap {
|
||||
match GLOBALS.signer.unwrap_giftwrap(event) {
|
||||
Ok(rumor) => {
|
||||
rumor_event = rumor.into_event_with_bad_signature();
|
||||
rumor_event.id = event.id; // lie, so it indexes it under the giftwrap
|
||||
event = &rumor_event;
|
||||
}
|
||||
Err(e) => {
|
||||
if matches!(e.kind, ErrorKind::NoPrivateKey) {
|
||||
// Store as unindexed for later indexing
|
||||
let bytes = vec![];
|
||||
self.db_unindexed_giftwraps()?
|
||||
.put(txn, event.id.as_slice(), &bytes)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !event.kind.is_feed_displayable() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let bytes = event.id.as_slice();
|
||||
|
||||
let mut pubkeys: HashSet<PublicKey> = HashSet::new();
|
||||
for (pubkeyhex, _, _) in event.people() {
|
||||
let pubkey = match PublicKey::try_from_hex_string(pubkeyhex.as_str(), false) {
|
||||
Ok(pk) => pk,
|
||||
Err(_) => continue,
|
||||
};
|
||||
pubkeys.insert(pubkey);
|
||||
}
|
||||
for pubkey in event.people_referenced_in_content() {
|
||||
pubkeys.insert(pubkey);
|
||||
}
|
||||
if !pubkeys.is_empty() {
|
||||
for pubkey in pubkeys.drain() {
|
||||
let mut key: Vec<u8> = pubkey.to_bytes();
|
||||
key.extend((i64::MAX - event.created_at.0).to_be_bytes().as_slice()); // reverse created_at
|
||||
self.db_event_references_person1()?.put(txn, &key, bytes)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
};
|
||||
|
||||
match rw_txn {
|
||||
Some(txn) => f(txn)?,
|
||||
None => {
|
||||
let mut txn = self.env.write_txn()?;
|
||||
f(&mut txn)?;
|
||||
txn.commit()?;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Read all events referencing a given person in reverse time order
|
||||
pub(crate) fn read_events_referencing_person1<F>(
|
||||
&self,
|
||||
pubkey: &PublicKey,
|
||||
since: Unixtime,
|
||||
f: F,
|
||||
) -> Result<Vec<Event>, Error>
|
||||
where
|
||||
F: Fn(&Event) -> bool,
|
||||
{
|
||||
let txn = self.env.read_txn()?;
|
||||
let now = Unixtime::now().unwrap();
|
||||
let mut start_key: Vec<u8> = pubkey.to_bytes();
|
||||
let mut end_key: Vec<u8> = start_key.clone();
|
||||
start_key.extend((i64::MAX - now.0).to_be_bytes().as_slice()); // work back from now
|
||||
end_key.extend((i64::MAX - since.0).to_be_bytes().as_slice()); // until since
|
||||
let range = (Bound::Included(&*start_key), Bound::Excluded(&*end_key));
|
||||
let iter = self.db_event_references_person1()?.range(&txn, &range)?;
|
||||
let mut events: Vec<Event> = Vec::new();
|
||||
for result in iter {
|
||||
let (_key, val) = result?;
|
||||
|
||||
// Take the event
|
||||
let id = Id(val[0..32].try_into()?);
|
||||
// (like read_event, but we supply our on transaction)
|
||||
if let Some(bytes) = self.db_events1()?.get(&txn, id.as_slice())? {
|
||||
let event = Event::read_from_buffer(bytes)?;
|
||||
if f(&event) {
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We have to sort these because (pubkey/unixtime) isn't unique.
|
||||
// The sort should be pretty fast given they are already nearly sorted.
|
||||
events.sort_by(|a, b| match b.created_at.cmp(&a.created_at) {
|
||||
Ordering::Equal => b.id.cmp(&a.id),
|
||||
ordered => ordered,
|
||||
});
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
}
|
125
gossip-lib/src/storage/event_tag_index1.rs
Normal file
125
gossip-lib/src/storage/event_tag_index1.rs
Normal file
@ -0,0 +1,125 @@
|
||||
use crate::error::{Error, ErrorKind};
|
||||
use crate::globals::GLOBALS;
|
||||
use crate::storage::{RawDatabase, Storage};
|
||||
use heed::{types::UnalignedSlice, DatabaseFlags, RwTxn};
|
||||
use nostr_types::{Event, EventKind, PublicKeyHex};
|
||||
use std::sync::Mutex;
|
||||
|
||||
// NOTE: "innerp" is a fake tag. We store events that reference a person internally under it.
|
||||
pub(super) const INDEXED_TAGS: [&str; 4] = ["a", "d", "p", "delegation"];
|
||||
|
||||
// TagKey:QUOTE:TagValue -> Id
|
||||
// (dup keys, so multiple Ids per key)
|
||||
// val: id.as_slice() | Id(val[0..32].try_into()?)
|
||||
|
||||
static EVENT_TAG_INDEX1_DB_CREATE_LOCK: Mutex<()> = Mutex::new(());
|
||||
static mut EVENT_TAG_INDEX1_DB: Option<RawDatabase> = None;
|
||||
|
||||
impl Storage {
|
||||
pub(super) fn db_event_tag_index1(&self) -> Result<RawDatabase, Error> {
|
||||
unsafe {
|
||||
if let Some(db) = EVENT_TAG_INDEX1_DB {
|
||||
Ok(db)
|
||||
} else {
|
||||
// Lock. This drops when anything returns.
|
||||
let _lock = EVENT_TAG_INDEX1_DB_CREATE_LOCK.lock();
|
||||
|
||||
// In case of a race, check again
|
||||
if let Some(db) = EVENT_TAG_INDEX1_DB {
|
||||
return Ok(db);
|
||||
}
|
||||
|
||||
// Create it. We know that nobody else is doing this and that
|
||||
// it cannot happen twice.
|
||||
let mut txn = self.env.write_txn()?;
|
||||
let db = self
|
||||
.env
|
||||
.database_options()
|
||||
.types::<UnalignedSlice<u8>, UnalignedSlice<u8>>()
|
||||
.flags(DatabaseFlags::DUP_SORT | DatabaseFlags::DUP_FIXED)
|
||||
.name("event_tag_index")
|
||||
.create(&mut txn)?;
|
||||
txn.commit()?;
|
||||
EVENT_TAG_INDEX1_DB = Some(db);
|
||||
Ok(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_event_tag_index1<'a>(
|
||||
&'a self,
|
||||
event: &Event,
|
||||
rw_txn: Option<&mut RwTxn<'a>>,
|
||||
) -> Result<(), Error> {
|
||||
let f = |txn: &mut RwTxn<'a>| -> Result<(), Error> {
|
||||
let mut event = event;
|
||||
|
||||
let mut rumor_event: Event;
|
||||
if event.kind == EventKind::GiftWrap {
|
||||
match GLOBALS.signer.unwrap_giftwrap(event) {
|
||||
Ok(rumor) => {
|
||||
rumor_event = rumor.into_event_with_bad_signature();
|
||||
rumor_event.id = event.id; // lie, so it indexes it under the giftwrap
|
||||
event = &rumor_event;
|
||||
}
|
||||
Err(e) => {
|
||||
if matches!(e.kind, ErrorKind::NoPrivateKey) {
|
||||
// Store as unindexed for later indexing
|
||||
let bytes = vec![];
|
||||
self.db_unindexed_giftwraps()?
|
||||
.put(txn, event.id.as_slice(), &bytes)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// our user's public key
|
||||
let pk: Option<PublicKeyHex> = self.read_setting_public_key().map(|p| p.into());
|
||||
|
||||
for tag in &event.tags {
|
||||
let tagname = tag.tagname();
|
||||
let value = match tag.value(1) {
|
||||
Ok(v) => v,
|
||||
Err(_) => continue, // no tag value, not indexable.
|
||||
};
|
||||
|
||||
// Only index tags we intend to lookup later by tag.
|
||||
// If that set changes, (1) add to this code and (2) do a reindex migration
|
||||
if !INDEXED_TAGS.contains(&&*tagname) {
|
||||
continue;
|
||||
}
|
||||
// For 'p' tags, only index them if 'p' is our user
|
||||
if tagname == "p" {
|
||||
match &pk {
|
||||
None => continue,
|
||||
Some(pk) => {
|
||||
if value != pk.as_str() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut key: Vec<u8> = tagname.as_bytes().to_owned();
|
||||
key.push(b'\"'); // double quote separator, unlikely to be inside of a tagname
|
||||
key.extend(value.as_bytes());
|
||||
let key = key!(&key); // limit the size
|
||||
let bytes = event.id.as_slice();
|
||||
self.db_event_tag_index()?.put(txn, key, bytes)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
};
|
||||
|
||||
match rw_txn {
|
||||
Some(txn) => f(txn)?,
|
||||
None => {
|
||||
let mut txn = self.env.write_txn()?;
|
||||
f(&mut txn)?;
|
||||
txn.commit()?;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -58,7 +58,7 @@ impl Storage {
|
||||
// also index the event
|
||||
self.write_event_ek_pk_index(event, Some(txn))?;
|
||||
self.write_event_ek_c_index(event, Some(txn))?;
|
||||
self.write_event_references_person(event, Some(txn))?;
|
||||
self.write_event_tag_index(event, Some(txn))?;
|
||||
for hashtag in event.hashtags() {
|
||||
if hashtag.is_empty() {
|
||||
continue;
|
||||
|
@ -2,12 +2,13 @@ use super::types::{Person2, PersonRelay1, Settings1, Settings2, Theme1, ThemeVar
|
||||
use super::Storage;
|
||||
use crate::error::{Error, ErrorKind};
|
||||
use crate::people::PersonList;
|
||||
use heed::RwTxn;
|
||||
use heed::types::UnalignedSlice;
|
||||
use heed::{DatabaseFlags, RwTxn};
|
||||
use nostr_types::{Event, Id, RelayUrl, Signature};
|
||||
use speedy::{Readable, Writable};
|
||||
|
||||
impl Storage {
|
||||
const MAX_MIGRATION_LEVEL: u32 = 10;
|
||||
const MAX_MIGRATION_LEVEL: u32 = 12;
|
||||
|
||||
pub(super) fn migrate(&self, mut level: u32) -> Result<(), Error> {
|
||||
if level > Self::MAX_MIGRATION_LEVEL {
|
||||
@ -57,9 +58,12 @@ impl Storage {
|
||||
let _ = self.db_events1()?;
|
||||
let _ = self.db_event_ek_pk_index1()?;
|
||||
let _ = self.db_event_ek_c_index1()?;
|
||||
let _ = self.db_event_references_person1()?;
|
||||
let _ = self.db_hashtags1()?;
|
||||
}
|
||||
10 => {
|
||||
let _ = self.db_events1()?;
|
||||
let _ = self.db_event_tag_index1()?;
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
Ok(())
|
||||
@ -111,6 +115,14 @@ impl Storage {
|
||||
tracing::info!("{prefix}: rewriting theme settings...");
|
||||
self.rewrite_theme_settings(txn)?;
|
||||
}
|
||||
10 => {
|
||||
tracing::info!("{prefix}: populating event tag index...");
|
||||
self.populate_event_tag_index(txn)?;
|
||||
}
|
||||
11 => {
|
||||
tracing::info!("{prefix}: removing now unused event_references_person index...");
|
||||
self.remove_event_references_person(txn)?;
|
||||
}
|
||||
_ => panic!("Unreachable migration level"),
|
||||
};
|
||||
|
||||
@ -516,4 +528,33 @@ impl Storage {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn populate_event_tag_index<'a>(&'a self, txn: &mut RwTxn<'a>) -> Result<(), Error> {
|
||||
let loop_txn = self.env.read_txn()?;
|
||||
for result in self.db_events1()?.iter(&loop_txn)? {
|
||||
let (_key, val) = result?;
|
||||
let event = Event::read_from_buffer(val)?;
|
||||
self.write_event_tag_index(&event, Some(txn))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_event_references_person<'a>(&'a self, txn: &mut RwTxn<'a>) -> Result<(), Error> {
|
||||
{
|
||||
let db = self
|
||||
.env
|
||||
.database_options()
|
||||
.types::<UnalignedSlice<u8>, UnalignedSlice<u8>>()
|
||||
.flags(DatabaseFlags::DUP_SORT | DatabaseFlags::DUP_FIXED)
|
||||
.name("event_references_person")
|
||||
.create(txn)?;
|
||||
|
||||
db.clear(txn)?;
|
||||
}
|
||||
|
||||
// heed doesn't expose mdb_drop(1) yet, so we can't actually remove this database.
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -19,8 +19,8 @@ pub mod types;
|
||||
// database implementations
|
||||
mod event_ek_c_index1;
|
||||
mod event_ek_pk_index1;
|
||||
mod event_references_person1;
|
||||
mod event_seen_on_relay1;
|
||||
mod event_tag_index1;
|
||||
mod event_viewed1;
|
||||
mod events1;
|
||||
mod hashtags1;
|
||||
@ -52,6 +52,8 @@ use speedy::{Readable, Writable};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::ops::Bound;
|
||||
|
||||
use self::event_tag_index1::INDEXED_TAGS;
|
||||
|
||||
// Macro to define read-and-write into "general" database, largely for settings
|
||||
// The type must implemented Speedy Readable and Writable
|
||||
macro_rules! def_setting {
|
||||
@ -178,7 +180,7 @@ impl Storage {
|
||||
// triggered into existence if their migration is necessary.
|
||||
let _ = self.db_event_ek_c_index()?;
|
||||
let _ = self.db_event_ek_pk_index()?;
|
||||
let _ = self.db_event_references_person()?;
|
||||
let _ = self.db_event_tag_index()?;
|
||||
let _ = self.db_events()?;
|
||||
let _ = self.db_event_seen_on_relay()?;
|
||||
let _ = self.db_event_viewed()?;
|
||||
@ -231,8 +233,8 @@ impl Storage {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn db_event_references_person(&self) -> Result<RawDatabase, Error> {
|
||||
self.db_event_references_person1()
|
||||
pub(crate) fn db_event_tag_index(&self) -> Result<RawDatabase, Error> {
|
||||
self.db_event_tag_index1()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@ -335,10 +337,10 @@ impl Storage {
|
||||
Ok(self.db_event_ek_c_index()?.len(&txn)?)
|
||||
}
|
||||
|
||||
/// The number of records in the event_references_person index table
|
||||
pub fn get_event_references_person_len(&self) -> Result<u64, Error> {
|
||||
/// The number of records in the event_tag index table
|
||||
pub fn get_event_tag_index_len(&self) -> Result<u64, Error> {
|
||||
let txn = self.env.read_txn()?;
|
||||
Ok(self.db_event_references_person()?.len(&txn)?)
|
||||
Ok(self.db_event_tag_index()?.len(&txn)?)
|
||||
}
|
||||
|
||||
/// The number of records in the relationships table
|
||||
@ -1462,7 +1464,7 @@ impl Storage {
|
||||
}
|
||||
|
||||
if sort {
|
||||
events.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
||||
events.sort_by(|a, b| b.created_at.cmp(&a.created_at).then(b.id.cmp(&a.id)));
|
||||
}
|
||||
|
||||
Ok(events)
|
||||
@ -1548,7 +1550,7 @@ impl Storage {
|
||||
|
||||
events.sort_by(|a, b| {
|
||||
// ORDER created_at desc
|
||||
b.created_at.cmp(&a.created_at)
|
||||
b.created_at.cmp(&a.created_at).then(b.id.cmp(&a.id))
|
||||
});
|
||||
|
||||
Ok(events)
|
||||
@ -1654,28 +1656,66 @@ impl Storage {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// We don't call this externally. Whenever we write an event, we do this.
|
||||
#[inline]
|
||||
fn write_event_references_person<'a>(
|
||||
fn write_event_tag_index<'a>(
|
||||
&'a self,
|
||||
event: &Event,
|
||||
rw_txn: Option<&mut RwTxn<'a>>,
|
||||
) -> Result<(), Error> {
|
||||
self.write_event_references_person1(event, rw_txn)
|
||||
self.write_event_tag_index1(event, rw_txn)
|
||||
}
|
||||
|
||||
/// Read all events referencing a given person in reverse time order
|
||||
#[inline]
|
||||
pub fn read_events_referencing_person<F>(
|
||||
/// Find events having a given tag, and passing the filter.
|
||||
/// Only some tags are indxed: "a", "d", "delegation", and "p" for the gossip user only
|
||||
pub fn find_tagged_events<F>(
|
||||
&self,
|
||||
pubkey: &PublicKey,
|
||||
since: Unixtime,
|
||||
tagname: &str,
|
||||
tagvalue: Option<&str>,
|
||||
f: F,
|
||||
sort: bool,
|
||||
) -> Result<Vec<Event>, Error>
|
||||
where
|
||||
F: Fn(&Event) -> bool,
|
||||
{
|
||||
self.read_events_referencing_person1(pubkey, since, f)
|
||||
// Make sure we are asking for something that we have indexed
|
||||
if !INDEXED_TAGS.contains(&tagname) {
|
||||
return Err(ErrorKind::TagNotIndexed(tagname.to_owned()).into());
|
||||
}
|
||||
|
||||
let mut ids: HashSet<Id> = HashSet::new();
|
||||
let txn = self.env.read_txn()?;
|
||||
|
||||
let mut start_key: Vec<u8> = tagname.as_bytes().to_owned();
|
||||
start_key.push(b'\"'); // double quote separator, unlikely to be inside of a tagname
|
||||
if let Some(tv) = tagvalue {
|
||||
start_key.extend(tv.as_bytes());
|
||||
}
|
||||
let start_key = key!(&start_key); // limit the size
|
||||
let iter = self.db_event_tag_index()?.prefix_iter(&txn, start_key)?;
|
||||
for result in iter {
|
||||
let (_key, val) = result?;
|
||||
// Take the event
|
||||
let id = Id(val[0..32].try_into()?);
|
||||
ids.insert(id);
|
||||
}
|
||||
|
||||
// Now that we have that Ids, fetch and filter the events
|
||||
let txn = self.env.read_txn()?;
|
||||
let mut events: Vec<Event> = Vec::new();
|
||||
for id in ids {
|
||||
// this is like self.read_event(), but we supply our existing transaction
|
||||
if let Some(bytes) = self.db_events()?.get(&txn, id.as_slice())? {
|
||||
let event = Event::read_from_buffer(bytes)?;
|
||||
if f(&event) {
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if sort {
|
||||
events.sort_by(|a, b| b.created_at.cmp(&a.created_at).then(b.id.cmp(&a.id)));
|
||||
}
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@ -2054,18 +2094,25 @@ impl Storage {
|
||||
Some(dmc) => dmc,
|
||||
None => continue,
|
||||
};
|
||||
map.entry(dmchannel.clone())
|
||||
.and_modify(|d| {
|
||||
d.latest_message = d.latest_message.max(time);
|
||||
d.message_count += 1;
|
||||
d.unread_message_count += unread;
|
||||
})
|
||||
.or_insert(DmChannelData {
|
||||
dm_channel: dmchannel,
|
||||
latest_message: time,
|
||||
message_count: 1,
|
||||
unread_message_count: unread,
|
||||
});
|
||||
if let Some(dmcdata) = map.get_mut(&dmchannel) {
|
||||
if time > dmcdata.latest_message_created_at {
|
||||
dmcdata.latest_message_created_at = time;
|
||||
dmcdata.latest_message_content = GLOBALS.signer.decrypt_message(event)?;
|
||||
}
|
||||
dmcdata.message_count += 1;
|
||||
dmcdata.unread_message_count += unread;
|
||||
} else {
|
||||
map.insert(
|
||||
dmchannel.clone(),
|
||||
DmChannelData {
|
||||
dm_channel: dmchannel,
|
||||
latest_message_created_at: time,
|
||||
latest_message_content: GLOBALS.signer.decrypt_message(event)?,
|
||||
message_count: 1,
|
||||
unread_message_count: unread,
|
||||
},
|
||||
);
|
||||
}
|
||||
} else if event.kind == EventKind::GiftWrap {
|
||||
if let Ok(rumor) = GLOBALS.signer.unwrap_giftwrap(event) {
|
||||
let rumor_event = rumor.into_event_with_bad_signature();
|
||||
@ -2074,24 +2121,35 @@ impl Storage {
|
||||
Some(dmc) => dmc,
|
||||
None => continue,
|
||||
};
|
||||
map.entry(dmchannel.clone())
|
||||
.and_modify(|d| {
|
||||
d.latest_message = d.latest_message.max(time);
|
||||
d.message_count += 1;
|
||||
d.unread_message_count += unread;
|
||||
})
|
||||
.or_insert(DmChannelData {
|
||||
dm_channel: dmchannel,
|
||||
latest_message: time,
|
||||
message_count: 1,
|
||||
unread_message_count: unread,
|
||||
});
|
||||
if let Some(dmcdata) = map.get_mut(&dmchannel) {
|
||||
if time > dmcdata.latest_message_created_at {
|
||||
dmcdata.latest_message_created_at = time;
|
||||
dmcdata.latest_message_content = rumor_event.content.clone();
|
||||
}
|
||||
dmcdata.message_count += 1;
|
||||
dmcdata.unread_message_count += unread;
|
||||
} else {
|
||||
map.insert(
|
||||
dmchannel.clone(),
|
||||
DmChannelData {
|
||||
dm_channel: dmchannel,
|
||||
latest_message_created_at: time,
|
||||
latest_message_content: rumor_event.content.clone(),
|
||||
message_count: 1,
|
||||
unread_message_count: unread,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut output: Vec<DmChannelData> = map.drain().map(|e| e.1).collect();
|
||||
output.sort_by(|a, b| b.latest_message.cmp(&a.latest_message));
|
||||
output.sort_by(|a, b| {
|
||||
b.latest_message_created_at
|
||||
.cmp(&a.latest_message_created_at)
|
||||
.then(b.unread_message_count.cmp(&a.unread_message_count))
|
||||
});
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
@ -2118,7 +2176,7 @@ impl Storage {
|
||||
)?;
|
||||
|
||||
// sort
|
||||
output.sort_by(|a, b| b.created_at.cmp(&a.created_at));
|
||||
output.sort_by(|a, b| b.created_at.cmp(&a.created_at).then(b.id.cmp(&a.id)));
|
||||
|
||||
Ok(output.iter().map(|e| e.id).collect())
|
||||
}
|
||||
@ -2133,7 +2191,7 @@ impl Storage {
|
||||
// Erase all indices first
|
||||
self.db_event_ek_pk_index()?.clear(txn)?;
|
||||
self.db_event_ek_c_index()?.clear(txn)?;
|
||||
self.db_event_references_person()?.clear(txn)?;
|
||||
self.db_event_tag_index()?.clear(txn)?;
|
||||
self.db_hashtags()?.clear(txn)?;
|
||||
|
||||
let loop_txn = self.env.read_txn()?;
|
||||
@ -2142,7 +2200,7 @@ impl Storage {
|
||||
let event = Event::read_from_buffer(val)?;
|
||||
self.write_event_ek_pk_index(&event, Some(txn))?;
|
||||
self.write_event_ek_c_index(&event, Some(txn))?;
|
||||
self.write_event_references_person(&event, Some(txn))?;
|
||||
self.write_event_tag_index(&event, Some(txn))?;
|
||||
for hashtag in event.hashtags() {
|
||||
if hashtag.is_empty() {
|
||||
continue;
|
||||
@ -2167,6 +2225,37 @@ impl Storage {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn rebuild_event_tags_index<'a>(
|
||||
&'a self,
|
||||
rw_txn: Option<&mut RwTxn<'a>>,
|
||||
) -> Result<(), Error> {
|
||||
let f = |txn: &mut RwTxn<'a>| -> Result<(), Error> {
|
||||
// Erase the index first
|
||||
self.db_event_tag_index()?.clear(txn)?;
|
||||
|
||||
let loop_txn = self.env.read_txn()?;
|
||||
for result in self.db_events()?.iter(&loop_txn)? {
|
||||
let (_key, val) = result?;
|
||||
let event = Event::read_from_buffer(val)?;
|
||||
self.write_event_tag_index(&event, Some(txn))?;
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
|
||||
match rw_txn {
|
||||
Some(txn) => {
|
||||
f(txn)?;
|
||||
}
|
||||
None => {
|
||||
let mut txn = self.env.write_txn()?;
|
||||
f(&mut txn)?;
|
||||
txn.commit()?;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read person lists
|
||||
pub fn read_person_lists(&self, pubkey: &PublicKey) -> Result<Vec<PersonList>, Error> {
|
||||
self.read_person_lists1(pubkey)
|
||||
|
@ -63,7 +63,7 @@ impl Storage {
|
||||
if let Some(event) = self.read_event(id)? {
|
||||
self.write_event_ek_pk_index(&event, Some(&mut txn))?;
|
||||
self.write_event_ek_c_index(&event, Some(&mut txn))?;
|
||||
self.write_event_references_person(&event, Some(&mut txn))?;
|
||||
self.write_event_tag_index(&event, Some(&mut txn))?;
|
||||
}
|
||||
self.db_unindexed_giftwraps1()?
|
||||
.delete(&mut txn, id.as_slice())?;
|
||||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 721 B |
@ -1,3 +1,4 @@
|
||||
# RELEASE
|
||||
|
||||
0. DON'T update dependencies. DON'T 'cargo update'. Do that kind of stuff right after
|
||||
releasing. Because that stuff presents risk.
|
||||
@ -9,9 +10,11 @@
|
||||
|
||||
2. Stabilize the code. Make all these happy:
|
||||
|
||||
$ cargo clippy
|
||||
$ cargo fmt
|
||||
$ cargo test
|
||||
````bash
|
||||
cargo clippy
|
||||
cargo fmt
|
||||
cargo test
|
||||
````
|
||||
|
||||
3. Edit Cargo.toml and change the version (remove the -unstable).
|
||||
Compile so you get a new Cargo.lock
|
||||
@ -28,24 +31,33 @@
|
||||
|
||||
5. Build the debian:
|
||||
|
||||
$ cd debian
|
||||
$ ./deb.sh
|
||||
````bash
|
||||
cd debian
|
||||
./deb.sh
|
||||
````
|
||||
|
||||
6. Build the appimage
|
||||
6. Build the appimage:
|
||||
|
||||
$ cd appimage
|
||||
$ cargo appimage --features="lang-cjk,video-ffmpeg"
|
||||
````bash
|
||||
cd appimage
|
||||
cargo appimage --features="lang-cjk,video-ffmpeg"
|
||||
````
|
||||
|
||||
7. Build the windows
|
||||
7. Build the windows:
|
||||
|
||||
$ cd windows
|
||||
Follow the windows/README.txt
|
||||
````bash
|
||||
cd windows
|
||||
````
|
||||
|
||||
8. Build the macos
|
||||
and follow the [Windows README](windows/README.md)
|
||||
|
||||
$ cd macos
|
||||
$ ./build_macos.sh
|
||||
$ ./build_macos_intel.sh
|
||||
8. Build the macos:
|
||||
|
||||
````bash
|
||||
cd macos
|
||||
./build_macos.sh
|
||||
./build_macos_intel.sh
|
||||
````
|
||||
|
||||
9. Bundle the files, create SHA256 hashes
|
||||
|
||||
@ -55,10 +67,8 @@
|
||||
|
||||
12. Announce release on nostr under gossip account
|
||||
|
||||
|
||||
-----------------
|
||||
|
||||
|
||||
This is a draft of the steps taken to make a release.
|
||||
I intend to flesh this out as I actually make releases.
|
||||
|
||||
@ -77,10 +87,9 @@ gossip
|
||||
├── nostr-types
|
||||
└── qrcode
|
||||
|
||||
|
||||
Try to push our dependency changes upstream:
|
||||
https://github.com/mikedilger/qrcode-rust (unlikely, stale for >3 years)
|
||||
https://github.com/mikedilger/egui
|
||||
<https://github.com/mikedilger/qrcode-rust> (unlikely, stale for >3 years)
|
||||
<https://github.com/mikedilger/egui>
|
||||
|
||||
nostr-types
|
||||
-- cargo update, and check for new versions, maybe update dependencies
|
||||
@ -118,19 +127,23 @@ gossip
|
||||
-- master
|
||||
-- version 0.N+1.0-unstable
|
||||
|
||||
-----------------------------
|
||||
-----------------
|
||||
|
||||
Package & Publish of gossip:
|
||||
|
||||
Package for windows:
|
||||
* main version, as .msi
|
||||
* main version with lang-cjk, as .msi
|
||||
|
||||
* main version, as .msi
|
||||
* main version with lang-cjk, as .msi
|
||||
|
||||
Package for debian:
|
||||
* main version, as .msi
|
||||
* main version with lang-cjk, as .msi
|
||||
|
||||
* main version, as .msi
|
||||
* main version with lang-cjk, as .msi
|
||||
|
||||
Create github release (it will create source tar files)
|
||||
* Post the windows .msi files
|
||||
* Post the debian .deb files
|
||||
|
||||
* Post the windows .msi files
|
||||
* Post the debian .deb files
|
||||
|
||||
Update aur.archlinux.org PKGBUILD
|
@ -39,7 +39,7 @@ cp macos_launch.sh $APP_DIR/Contents/MacOS/$APP_NAME
|
||||
echo "Copying Icon"
|
||||
mkdir -p $APP_DIR/Contents/Resources
|
||||
cat Info.plist | sed s/__VERSION__/$VERSION/g > $APP_DIR/Contents/Info.plist
|
||||
cp ../../$NAME.png ../../$NAME.svg $APP_DIR/Contents/Resources
|
||||
cp ../../logo/$NAME.png ../../logo/$NAME.svg $APP_DIR/Contents/Resources
|
||||
|
||||
echo "Creating dmg"
|
||||
mkdir -p $APP_NAME
|
||||
|
@ -39,7 +39,7 @@ cp macos_launch.sh $APP_DIR/Contents/MacOS/$APP_NAME
|
||||
echo "Copying Icon"
|
||||
mkdir -p $APP_DIR/Contents/Resources
|
||||
cat Info.plist | sed s/__VERSION__/$VERSION/g > $APP_DIR/Contents/Info.plist
|
||||
cp ../../$NAME.png ../../$NAME.svg $APP_DIR/Contents/Resources
|
||||
cp ../../logo/$NAME.png ../../logo/$NAME.svg $APP_DIR/Contents/Resources
|
||||
|
||||
echo "Creating dmg"
|
||||
mkdir -p $APP_NAME
|
||||
|
52
packaging/windows/README.md
Normal file
52
packaging/windows/README.md
Normal file
@ -0,0 +1,52 @@
|
||||
# WINDOWS
|
||||
|
||||
Prerequisite for packaging:
|
||||
|
||||
* You need Wix 4 tools installed, probably with DOTNET installed first.
|
||||
|
||||
Compile:
|
||||
|
||||
````dos
|
||||
rustup update
|
||||
cargo build --features=lang-cjk --release
|
||||
````
|
||||
|
||||
Copy the binary to the packaging diretory
|
||||
|
||||
````dos
|
||||
cp ..\..\target\release\gossip.exe .
|
||||
````
|
||||
|
||||
Copy the gossip.png here
|
||||
|
||||
````dos
|
||||
cp ..\..\logo\gossip.png .
|
||||
````
|
||||
|
||||
For new versions of gossip, update `gossip.wxs`:
|
||||
|
||||
* UPDATE the Package.Version, SummaryInformation.Description
|
||||
* UPDATE the Package.ProductCode GUID to a new one
|
||||
* KEEP the UpgradeCode GUID (it should never change, it ties different versions together)
|
||||
* Change a component GUID ONLY IF the absolute path changes.
|
||||
|
||||
Packaging:
|
||||
|
||||
````dos
|
||||
wix build gossip.VERSION.wxs
|
||||
````
|
||||
|
||||
Upload to github releases.
|
||||
|
||||
----
|
||||
To install the package, either double-click the MSI, or
|
||||
|
||||
````dos
|
||||
msiexec gossip.msi
|
||||
````
|
||||
|
||||
To remove the package from your windows computer:
|
||||
|
||||
````dos
|
||||
msiexec /x gossip.msi
|
||||
````
|
@ -1,39 +0,0 @@
|
||||
|
||||
Prerequisite for packaging:
|
||||
|
||||
* You need Wix 4 tools installed, probably with DOTNET installed first.
|
||||
|
||||
Compile:
|
||||
|
||||
$ rustup update
|
||||
$ cargo build --features=lang-cjk --release
|
||||
|
||||
Copy the binary to the packaging diretory
|
||||
|
||||
$ cp ..\..\target\release\gossip.exe .
|
||||
|
||||
Copy the gossip.png here
|
||||
|
||||
$ cp ..\..\gossip.png .
|
||||
|
||||
For new versions of gossip, update gossip.wxs
|
||||
* UPDATE the Package.Version, SummaryInformation.Description
|
||||
* UPDATE the Package.ProductCode GUID to a new one
|
||||
* KEEP the UpgradeCode GUID (it should never change, it ties different versions together)
|
||||
* Change a component GUID ONLY IF the absolute path changes.
|
||||
|
||||
Packaging:
|
||||
|
||||
$ wix build gossip.VERSION.wxs
|
||||
|
||||
Upload to github releases.
|
||||
|
||||
|
||||
----
|
||||
To install the package, either double-click the MSI, or
|
||||
|
||||
$ msiexec gossip.msi
|
||||
|
||||
To remove the package from your windows computer:
|
||||
|
||||
$ msiexec /x gossip.msi
|
Loading…
Reference in New Issue
Block a user