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:
Bu5hm4nn 2023-10-09 17:38:20 -06:00
commit 0e72b40556
58 changed files with 1020 additions and 1890 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
perf.data
perf.data.old
flamegraph.svg
.vscode/

View File

@ -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

View File

@ -1,4 +0,0 @@
#!/bin/bash
cargo build --features=lang-cjk,video-ffmpeg && \
RUST_BACKTRACE=1 RUST_LOG="info,gossip=debug" ./target/debug/gossip

View File

@ -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
View 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
````

View File

@ -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

View File

@ -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

View File

@ -1,3 +0,0 @@
#!/bin/bash
find . -name .git -prune -o -name target -prune -o -type f -exec grep -H "$1" {} \;

View File

@ -1,4 +0,0 @@
#!/bin/bash
find . -name .git -prune -o -name target -prune -o -name "$1" -print

View File

@ -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
View 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");
}

View File

@ -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)
{

View File

@ -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());

View File

@ -284,7 +284,7 @@ fn render_note_inner(
ui.add_space(3.0);
GossipUi::render_person_name_line(app, ui, &note.author);
GossipUi::render_person_name_line(app, ui, &note.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 &note.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),
);
}
}
}

View File

@ -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();

View File

@ -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);

View File

@ -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();

View File

@ -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());
}
},
);
}
}
});
}

View File

@ -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(

View File

@ -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)

View File

@ -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);

View File

@ -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 {

View File

@ -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);
});

View File

@ -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| {

View File

@ -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| {

View File

@ -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() {

View File

@ -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());
}

View File

@ -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| {

View File

@ -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);

View File

@ -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
}
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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(

View File

@ -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));
}
}
}

View File

@ -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)
*/
}

View File

@ -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);

View File

@ -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 => {

View File

@ -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();

View File

@ -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");
}

View File

@ -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,
}

View File

@ -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"),

View File

@ -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) => {

View File

@ -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(())

View File

@ -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)
}
}

View 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(())
}
}

View File

@ -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;

View File

@ -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(())
}
}

View File

@ -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)

View File

@ -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())?;

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 721 B

View File

@ -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

View File

@ -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

View File

@ -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

View 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
````

View File

@ -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