diff options
| author | Marvin W <git@larma.de> | 2026-03-14 13:42:58 +0800 |
|---|---|---|
| committer | Marvin W <git@larma.de> | 2026-04-08 19:02:44 +0200 |
| commit | 64757cbe2572259ad201f4c691aeb545088ae42a (patch) | |
| tree | 6c3de38252417500ab1234687bd413e29dcc4d1b /main/src | |
| parent | 82da21c1a67d5a50d0a4940b5e3c687929b041c2 (diff) | |
Add implementation for XEP-0202 Entity Time
Diffstat (limited to 'main/src')
| -rw-r--r-- | main/src/ui/chat_input/view.vala | 97 | ||||
| -rw-r--r-- | main/src/ui/conversation_details.vala | 66 | ||||
| -rw-r--r-- | main/src/ui/util/preference_group.vala | 65 | ||||
| -rw-r--r-- | main/src/view_model/conversation_details.vala | 1 | ||||
| -rw-r--r-- | main/src/view_model/preferences_dialog.vala | 1 | ||||
| -rw-r--r-- | main/src/view_model/preferences_row.vala | 1 | ||||
| -rw-r--r-- | main/src/windows/preferences_window/general_preferences_page.vala | 3 |
7 files changed, 185 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 diff --git a/main/src/view_model/conversation_details.vala b/main/src/view_model/conversation_details.vala index 26379dab..c8da2ee0 100644 --- a/main/src/view_model/conversation_details.vala +++ b/main/src/view_model/conversation_details.vala @@ -84,6 +84,7 @@ public class Dino.Ui.Model.ConversationDetails : Object { public string? data_form_bak; public bool blocked { get; set; } public bool domain_blocked { get; set; } + public int utc_offset_minutes { get; set; default = int.MIN; } public GLib.ListStore members = new GLib.ListStore(typeof(Ui.Model.ConferenceMember)); diff --git a/main/src/view_model/preferences_dialog.vala b/main/src/view_model/preferences_dialog.vala index af00494d..20e11ae3 100644 --- a/main/src/view_model/preferences_dialog.vala +++ b/main/src/view_model/preferences_dialog.vala @@ -109,6 +109,7 @@ public class Dino.Ui.ViewModel.PreferencesDialog : Object { var settings = Dino.Application.get_default().settings; settings.bind_property("send-typing", general_page, "send-typing", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); settings.bind_property("send-marker", general_page, "send-marker", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); + settings.bind_property("share-time", general_page, "share-time", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); settings.bind_property("notifications", general_page, "notifications", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); settings.bind_property("convert-utf8-smileys", general_page, "convert-emojis", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); } diff --git a/main/src/view_model/preferences_row.vala b/main/src/view_model/preferences_row.vala index 4ca2f2df..d0f64979 100644 --- a/main/src/view_model/preferences_row.vala +++ b/main/src/view_model/preferences_row.vala @@ -6,6 +6,7 @@ using Gee; namespace Dino.Ui.ViewModel.PreferencesRow { public abstract class Any : Object { public string title { get; set; } + public bool visible { get; set; default=true; } public string? media_type { get; set; } public string? media_uri { get; set; } diff --git a/main/src/windows/preferences_window/general_preferences_page.vala b/main/src/windows/preferences_window/general_preferences_page.vala index 1e81a1b7..0dcf2118 100644 --- a/main/src/windows/preferences_window/general_preferences_page.vala +++ b/main/src/windows/preferences_window/general_preferences_page.vala @@ -3,6 +3,7 @@ using Gtk; public class Dino.Ui.ViewModel.GeneralPreferencesPage : Object { public bool send_typing { get; set; } public bool send_marker { get; set; } + public bool share_time { get; set; } public bool notifications { get; set; } public bool convert_emojis { get; set; } } @@ -11,6 +12,7 @@ public class Dino.Ui.ViewModel.GeneralPreferencesPage : Object { public class Dino.Ui.GeneralPreferencesPage : Adw.PreferencesPage { [GtkChild] private unowned Adw.SwitchRow typing_row; [GtkChild] private unowned Adw.SwitchRow marker_row; + [GtkChild] private unowned Adw.SwitchRow time_row; [GtkChild] private unowned Adw.SwitchRow notification_row; [GtkChild] private unowned Adw.SwitchRow emoji_row; @@ -29,6 +31,7 @@ public class Dino.Ui.GeneralPreferencesPage : Adw.PreferencesPage { model_bindings = new Binding[] { model.bind_property("send-typing", typing_row, "active", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL), model.bind_property("send-marker", marker_row, "active", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL), + model.bind_property("share-time", time_row, "active", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL), model.bind_property("notifications", notification_row, "active", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL), model.bind_property("convert-emojis", emoji_row, "active", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL) }; |
