aboutsummaryrefslogtreecommitdiff
path: root/main/src/ui
diff options
context:
space:
mode:
authorMarvin W <git@larma.de>2026-03-14 13:42:58 +0800
committerMarvin W <git@larma.de>2026-04-08 19:02:44 +0200
commit64757cbe2572259ad201f4c691aeb545088ae42a (patch)
tree6c3de38252417500ab1234687bd413e29dcc4d1b /main/src/ui
parent82da21c1a67d5a50d0a4940b5e3c687929b041c2 (diff)
Add implementation for XEP-0202 Entity Time
Diffstat (limited to 'main/src/ui')
-rw-r--r--main/src/ui/chat_input/view.vala97
-rw-r--r--main/src/ui/conversation_details.vala66
-rw-r--r--main/src/ui/util/preference_group.vala65
3 files changed, 179 insertions, 49 deletions
diff --git a/main/src/ui/chat_input/view.vala b/main/src/ui/chat_input/view.vala
index dbb1f74e..8b57a08d 100644
--- a/main/src/ui/chat_input/view.vala
+++ b/main/src/ui/chat_input/view.vala
@@ -26,11 +26,18 @@ public class View : Box {
[GtkChild] public unowned MenuButton encryption_button;
[GtkChild] public unowned Separator file_separator;
[GtkChild] public unowned Label chat_input_status;
+ [GtkChild] public unowned Box unavailable_box;
+ [GtkChild] public unowned Image unavailable_icon;
+ [GtkChild] public unowned Label unavailable_label;
public EncryptionButton encryption_widget;
+ private bool is_dnd = false;
+ private bool is_night = false;
+
public View init(StreamInteractor stream_interactor) {
this.stream_interactor = stream_interactor;
+ stream_interactor.get_module(PresenceManager.IDENTITY).show_received.connect(on_show_received);
encryption_widget = new EncryptionButton(stream_interactor, encryption_button);
@@ -61,9 +68,99 @@ public class View : Box {
chat_text_view.text_view.buffer.text = entry_cache[conversation];
}
+ update_unavailable_message();
+
do_focus();
}
+ private void on_show_received(Jid jid, Account account) {
+ if (this.conversation.type_ == Conversation.Type.CHAT && this.conversation.counterpart.equals_bare(jid)) {
+ update_unavailable_message();
+ }
+ }
+
+ private void update_unavailable_message() {
+ if (conversation.type_ != Conversation.Type.CHAT) {
+ is_dnd = false;
+ is_night = false;
+ set_unavailable_message(null, null);
+ } else {
+ PresenceManager presence_manager = stream_interactor.get_module(PresenceManager.IDENTITY);
+ Gee.List<Jid>? full_jids = presence_manager.get_full_jids(conversation.counterpart, conversation.account);
+ if (full_jids != null) {
+ foreach (Jid full_jid in full_jids) {
+ if (presence_manager.get_last_show(full_jid, conversation.account) == Presence.Stanza.SHOW_DND) {
+ if (update_utc_offset_minutes_timeout != 0) {
+ Source.remove(update_utc_offset_minutes_timeout);
+ update_utc_offset_minutes_timeout = 0;
+ }
+ is_dnd = true;
+ set_unavailable_message(Markup.printf_escaped(_("%s has <b>paused their notifications</b>"), Util.get_conversation_display_name(stream_interactor, conversation)), "dino-bell-large-none-symbolic", true);
+ return;
+ }
+ }
+ }
+ is_dnd = false;
+ update_utc_offset_minutes();
+ }
+ }
+
+ private uint update_utc_offset_minutes_timeout = 0;
+ private void update_utc_offset_minutes() {
+ Conversation conversation = this.conversation;
+ if (update_utc_offset_minutes_timeout != 0) {
+ Source.remove(update_utc_offset_minutes_timeout);
+ update_utc_offset_minutes_timeout = 0;
+ }
+ if (conversation.type_ == Conversation.Type.CHAT) {
+ stream_interactor.get_module(EntityInfo.IDENTITY).get_utc_offset_minutes_for_bare_jid.begin(conversation.account, conversation.counterpart, (_, res) => {
+ set_utc_offset_minutes(conversation, stream_interactor.get_module(EntityInfo.IDENTITY).get_utc_offset_minutes_for_bare_jid.end(res));
+ update_utc_offset_minutes_timeout = WeakTimeout.add_once(60000 - (int) (new DateTime.now_utc().get_seconds()*1000d), this, update_utc_offset_minutes);
+ });
+ }
+ }
+
+ private void set_utc_offset_minutes(Conversation conversation, int utc_offset_minutes) {
+ if (this.conversation != conversation) return;
+ string? display_time = null;
+ if (utc_offset_minutes != int.MIN && new DateTime.now_local().get_utc_offset() / TimeSpan.MINUTE != utc_offset_minutes) {
+ var datetime = new DateTime.now(new TimeZone.offset(utc_offset_minutes * 60));
+ if (datetime.get_hour() >= 22 || datetime.get_hour() < 8) {
+ display_time = datetime.format(Util.is_24h_format() ?
+ /* xgettext:no-c-format */ /* Time in 24h format (w/o seconds) */ _("%H∶%M") :
+ /* xgettext:no-c-format */ /* Time in 12h format (w/o seconds) */ _("%l∶%M %p"));
+ }
+ }
+ is_night = (display_time != null);
+ if (!is_dnd) {
+ if (!is_night) {
+ set_unavailable_message(null, null);
+ } else {
+ set_unavailable_message(Markup.printf_escaped(_("It's <b>%s</b> for %s"), display_time, Util.get_conversation_display_name(stream_interactor, conversation)), "dino-clock-alt-symbolic", true);
+ }
+ }
+ }
+
+ private void set_unavailable_message(string? message, string? icon, bool use_markup = false) {
+ if (message == null) {
+ unavailable_box.visible = false;
+ } else {
+ if (use_markup) {
+ unavailable_label.set_markup(message);
+ } else {
+ unavailable_label.use_markup = use_markup;
+ unavailable_label.label = message;
+ }
+ unavailable_box.visible = true;
+ }
+ if (icon == null) {
+ unavailable_icon.opacity = 0;
+ } else {
+ unavailable_icon.icon_name = icon;
+ unavailable_icon.opacity = 1;
+ }
+ }
+
public void set_input_state(Plugins.InputFieldStatus.MessageType message_type) {
switch (message_type) {
case Plugins.InputFieldStatus.MessageType.NONE:
diff --git a/main/src/ui/conversation_details.vala b/main/src/ui/conversation_details.vala
index 404f357d..f9bae672 100644
--- a/main/src/ui/conversation_details.vala
+++ b/main/src/ui/conversation_details.vala
@@ -20,6 +20,11 @@ namespace Dino.Ui.ConversationDetails {
model.data_form_bak = model.data_form.stanza_node.to_string();
});
}
+ if (conversation.type_ == Conversation.Type.CHAT) {
+ stream_interactor.get_module(EntityInfo.IDENTITY).get_utc_offset_minutes_for_bare_jid.begin(conversation.account, conversation.counterpart, (_, res) => {
+ model.utc_offset_minutes = stream_interactor.get_module(EntityInfo.IDENTITY).get_utc_offset_minutes_for_bare_jid.end(res);
+ });
+ }
}
public void bind_dialog(Model.ConversationDetails model, ViewModel.ConversationDetails view_model, StreamInteractor stream_interactor) {
@@ -100,7 +105,7 @@ namespace Dino.Ui.ConversationDetails {
view_model.pin_changed.connect(() => {
model.conversation.pinned = model.conversation.pinned == 1 ? 0 : 1;
});
- view_model.block_changed.connect((action) => {
+ view_model.block_changed.connect((view_model, action) => {
switch (action) {
case USER:
stream_interactor.get_module(BlockingManager.IDENTITY).block(model.conversation.account, model.conversation.counterpart);
@@ -133,35 +138,67 @@ namespace Dino.Ui.ConversationDetails {
}
});
- view_model.notification_flipped.connect(() => {
+ view_model.notification_flipped.connect((view_model) => {
model.conversation.notify_setting = view_model.notification == ON ? Conversation.NotifySetting.OFF : Conversation.NotifySetting.ON;
});
}
+ private static string format_utc_offset_minutes(int utc_offset_minutes) {
+ var datetime = new DateTime.now(new TimeZone.offset(utc_offset_minutes * 60));
+ return datetime.format(Util.is_24h_format() ?
+ /* xgettext:no-c-format */ /* Weekday and time in 24h format (w/o seconds) */ _("%A, %H∶%M") :
+ /* xgettext:no-c-format */ /* Weekday and time in 12h format (w/o seconds) */ _("%A, %l∶%M %p"));
+ }
+
+ private uint get_interval_till_next_full_minute() {
+ return 60000 - (int) (new DateTime.now_utc().get_seconds()*1000d);
+ }
+
+ private static void notify_binding_once(Binding binding) {
+ binding.source.notify_property(binding.source_property);
+ }
+
public void set_about_rows(Model.ConversationDetails model, ViewModel.ConversationDetails view_model, StreamInteractor stream_interactor) {
+ Conversation conversation = model.conversation;
view_model.about_rows.append(new ViewModel.PreferencesRow.Text() {
title = _("XMPP Address"),
- text = model.conversation.counterpart.to_string()
+ text = conversation.counterpart.to_string()
});
- if (model.conversation.type_ == Conversation.Type.CHAT) {
+ if (conversation.type_ == Conversation.Type.CHAT) {
+ var display_name = model.display_name;
var about_row = new ViewModel.PreferencesRow.Entry() {
title = _("Display name"),
- text = model.display_name.display_name
+ text = display_name.display_name
};
- about_row.changed.connect(() => {
- if (about_row.text != model.display_name.display_name) {
- stream_interactor.get_module(RosterManager.IDENTITY).set_jid_handle(model.conversation.account, model.conversation.counterpart, about_row.text);
+ about_row.changed.connect((about_row) => {
+ if (about_row.text != display_name.display_name) {
+ stream_interactor.get_module(RosterManager.IDENTITY).set_jid_handle(conversation.account, conversation.counterpart, about_row.text);
}
});
view_model.about_rows.append(about_row);
+ var time_row = new ViewModel.PreferencesRow.Text() {
+ title = _("Local Time")
+ };
+ model.bind_property("utc-offset-minutes", time_row, "visible", BindingFlags.SYNC_CREATE, (binding, from, ref to) => {
+ int utc_offset_minutes = (int) from;
+ to = utc_offset_minutes != int.MIN;
+ return true;
+ });
+ model.bind_property("utc-offset-minutes", time_row, "text", BindingFlags.SYNC_CREATE, (binding, from, ref to) => {
+ var interval = get_interval_till_next_full_minute();
+ to = format_utc_offset_minutes((int) from);
+ WeakTimeout.add_once(interval, binding, notify_binding_once);
+ return true;
+ });
+ view_model.about_rows.append(time_row);
}
- if (model.conversation.type_ == Conversation.Type.GROUPCHAT) {
- var topic = stream_interactor.get_module(MucManager.IDENTITY).get_groupchat_subject(model.conversation.counterpart, model.conversation.account);
+ if (conversation.type_ == Conversation.Type.GROUPCHAT) {
+ var topic = stream_interactor.get_module(MucManager.IDENTITY).get_groupchat_subject(conversation.counterpart, conversation.account);
Ui.ViewModel.PreferencesRow.Any preferences_row = null;
- Jid? own_muc_jid = stream_interactor.get_module(MucManager.IDENTITY).get_own_jid(model.conversation.counterpart, model.conversation.account);
+ Jid? own_muc_jid = stream_interactor.get_module(MucManager.IDENTITY).get_own_jid(conversation.counterpart, conversation.account);
if (own_muc_jid != null) {
- Xep.Muc.Role? own_role = stream_interactor.get_module(MucManager.IDENTITY).get_role(own_muc_jid, model.conversation.account);
+ Xep.Muc.Role? own_role = stream_interactor.get_module(MucManager.IDENTITY).get_role(own_muc_jid, conversation.account);
if (own_role != null) {
if (own_role == MODERATOR) {
var preferences_row_entry = new ViewModel.PreferencesRow.Entry() {
@@ -170,7 +207,7 @@ namespace Dino.Ui.ConversationDetails {
};
preferences_row_entry.changed.connect(() => {
if (preferences_row_entry.text != topic) {
- stream_interactor.get_module(MucManager.IDENTITY).change_subject(model.conversation.account, model.conversation.counterpart, preferences_row_entry.text);
+ stream_interactor.get_module(MucManager.IDENTITY).change_subject(conversation.account, conversation.counterpart, preferences_row_entry.text);
}
});
preferences_row = preferences_row_entry;
@@ -205,8 +242,9 @@ namespace Dino.Ui.ConversationDetails {
});
Plugins.ContactDetails contact_details = new Plugins.ContactDetails();
+ var settings_rows = dialog.model.settings_rows;
contact_details.add_settings_action_row.connect((entry_row_model) => {
- dialog.model.settings_rows.append((Ui.ViewModel.PreferencesRow.Any) entry_row_model);
+ settings_rows.append((Ui.ViewModel.PreferencesRow.Any) entry_row_model);
});
Application app = GLib.Application.get_default() as Application;
app.plugin_registry.register_contact_details_entry(new ContactDetails.SettingsProvider(stream_interactor));
diff --git a/main/src/ui/util/preference_group.vala b/main/src/ui/util/preference_group.vala
index aa680e68..3911bb80 100644
--- a/main/src/ui/util/preference_group.vala
+++ b/main/src/ui/util/preference_group.vala
@@ -47,63 +47,55 @@ namespace Dino.Ui.Util {
return preference_group;
}
+ private static bool null_string_to_empty(GLib.Binding binding, GLib.Value from_value, ref GLib.Value to_value) {
+ var str = (string) from_value;
+ to_value = str ?? "";
+ return true;
+ }
+
public Adw.PreferencesRow? row_to_preference_row(ViewModel.PreferencesRow.Any preferences_row) {
+ Adw.PreferencesRow? view = null;
+
var entry_view_model = preferences_row as ViewModel.PreferencesRow.Entry;
if (entry_view_model != null) {
- Adw.EntryRow view = new Adw.EntryRow() { title = entry_view_model.title, show_apply_button=true };
+ view = new Adw.EntryRow() { show_apply_button=true };
if (preferences_row.media_uri != null) {
var bytes = Xmpp.get_data_for_uri(preferences_row.media_uri);
Picture picture = new Picture.for_paintable(Texture.from_bytes(bytes));
- view.add_suffix(picture);
+ ((Adw.EntryRow) view).add_suffix(picture);
}
- entry_view_model.bind_property("text", view, "text", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL, (_, from, ref to) => {
- var str = (string) from;
- to = str ?? "";
- return true;
- });
- view.apply.connect(() => {
+ entry_view_model.bind_property("text", view, "text", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL, null_string_to_empty);
+ ((Adw.EntryRow) view).apply.connect(() => {
entry_view_model.changed();
});
- return view;
}
var password_view_model = preferences_row as ViewModel.PreferencesRow.PrivateText;
if (password_view_model != null) {
- Adw.PasswordEntryRow view = new Adw.PasswordEntryRow() { title = password_view_model.title, show_apply_button=true };
- password_view_model.bind_property("text", view, "text", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL, (_, from, ref to) => {
- var str = (string) from;
- to = str ?? "";
- return true;
- });
- view.apply.connect(() => {
+ view = new Adw.PasswordEntryRow() { show_apply_button=true };
+ password_view_model.bind_property("text", view, "text", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL, null_string_to_empty);
+ ((Adw.EntryRow) view).apply.connect(() => {
password_view_model.changed();
});
-
- return view;
}
var row_text = preferences_row as ViewModel.PreferencesRow.Text;
if (row_text != null) {
- var view = new Adw.ActionRow() {
- title = row_text.title,
- subtitle = row_text.text,
+ view = new Adw.ActionRow() {
subtitle_selectable = true
};
+ row_text.bind_property("text", view, "subtitle", BindingFlags.SYNC_CREATE, null_string_to_empty);
view.add_css_class("property");
Util.force_css(view, "row.property > box.header > box.title > .title { font-weight: 400; font-size: 9pt; opacity: 0.55; }");
Util.force_css(view, "row.property > box.header > box.title > .subtitle { font-size: inherit; opacity: 1; }");
- return view;
}
var toggle_view_model = preferences_row as ViewModel.PreferencesRow.Toggle;
if (toggle_view_model != null) {
- var view = new Adw.ActionRow() { title = toggle_view_model.title, subtitle = toggle_view_model.subtitle ?? "" };
- var toggle = new Switch() { valign = Align.CENTER };
- view.activatable_widget = toggle;
- view.add_suffix(toggle);
- toggle_view_model.bind_property("state", toggle, "active", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
- return view;
+ view = new Adw.SwitchRow();
+ toggle_view_model.bind_property("subtitle", view, "subtitle", BindingFlags.SYNC_CREATE, null_string_to_empty);
+ toggle_view_model.bind_property("state", view, "active", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
}
var combobox_view_model = preferences_row as ViewModel.PreferencesRow.ComboBox;
@@ -112,21 +104,24 @@ namespace Dino.Ui.Util {
foreach (string text in combobox_view_model.items) {
string_list.append(text);
}
- var view = new Adw.ComboRow() { title = combobox_view_model.title, subtitle = combobox_view_model.subtitle ?? "" };
- view.model = string_list;
+ view = new Adw.ComboRow();
+ combobox_view_model.bind_property("subtitle", view, "subtitle", BindingFlags.SYNC_CREATE, null_string_to_empty);
+ ((Adw.ComboRow)view).model = string_list;
combobox_view_model.bind_property("active-item", view, "selected", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
- return view;
}
var button_view_model = preferences_row as ViewModel.PreferencesRow.Button;
if (button_view_model != null) {
- var view = new Adw.ActionRow() { title = button_view_model.title, subtitle = button_view_model.subtitle ?? "" };
+ view = new Adw.ActionRow();
+ button_view_model.bind_property("subtitle", view, "subtitle", BindingFlags.SYNC_CREATE, null_string_to_empty);
var button = new Button.with_label(button_view_model.button_text) { valign = Align.CENTER };
- view.add_suffix(button);
+ ((Adw.ActionRow)view).add_suffix(button);
button.clicked.connect(() => button_view_model.clicked());
- return view;
}
- return null;
+ preferences_row.bind_property("title", view, "title", BindingFlags.SYNC_CREATE);
+ preferences_row.bind_property("visible", view, "visible", BindingFlags.SYNC_CREATE);
+
+ return view;
}
} \ No newline at end of file