diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e9a7dd14..a04b0f33 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,10 +5,35 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v2 + with: + fetch-depth: 0 - run: sudo apt-get update - run: sudo apt-get remove libunwind-14-dev - - run: sudo apt-get install -y build-essential gettext cmake valac libgee-0.8-dev libsqlite3-dev libgtk-4-dev libnotify-dev libgpgme-dev libsoup2.4-dev libgcrypt20-dev libqrencode-dev libgspell-1-dev libnice-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libsrtp2-dev libwebrtc-audio-processing-dev libadwaita-1-dev + - run: sudo apt-get install -y build-essential gettext cmake valac libgee-0.8-dev libsqlite3-dev libgtk-4-dev libnotify-dev libgpgme-dev libsoup2.4-dev libgcrypt20-dev libqrencode-dev libnice-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libsrtp2-dev libwebrtc-audio-processing-dev libadwaita-1-dev - run: ./configure --with-tests --with-libsignal-in-tree - run: make - run: build/xmpp-vala-test - run: build/signal-protocol-vala-test + build-meson: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - run: sudo apt-get update + - run: sudo apt-get remove libunwind-14-dev + - run: sudo apt-get install -y build-essential gettext libadwaita-1-dev libgee-0.8-dev libgtk-4-dev libsqlite3-dev meson valac + - run: meson setup build + - run: meson compile -C build + build-flatpak: + runs-on: ubuntu-22.04 + container: + image: bilelmoussaoui/flatpak-github-actions:gnome-44 + options: --privileged + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: flatpak/flatpak-github-actions/flatpak-builder@v6.1 + with: + manifest-path: im.dino.Dino.json \ No newline at end of file diff --git a/cmake/FindGspell.cmake b/cmake/FindGspell.cmake deleted file mode 100644 index de29ed1d..00000000 --- a/cmake/FindGspell.cmake +++ /dev/null @@ -1,14 +0,0 @@ -include(PkgConfigWithFallback) -find_pkg_config_with_fallback(Gspell - PKG_CONFIG_NAME gspell-1 - LIB_NAMES gspell-1 - INCLUDE_NAMES gspell.h - INCLUDE_DIR_SUFFIXES gspell-1 gspell-1/gspell - DEPENDS GTK3 -) - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(Gspell - REQUIRED_VARS Gspell_LIBRARY - VERSION_VAR Gspell_VERSION) - diff --git a/dino.doap b/dino.doap index 819777b0..0de627cf 100644 --- a/dino.doap +++ b/dino.doap @@ -227,24 +227,28 @@ complete + 0.1 complete + 0.1 complete + 0.1 partial + 0.1 @@ -252,6 +256,7 @@ complete For use with XEP-0261 + 0.1 @@ -259,12 +264,14 @@ deprecated Migrating to XEP-0402 if supported by server + 0.1 complete + 0.1 @@ -272,6 +279,7 @@ partial Only for viewing avatars + 0.1 @@ -279,12 +287,14 @@ partial For use with XEP-0313 + 0.1 partial + 0.1 @@ -292,6 +302,7 @@ partial For use with XEP-0260 + 0.1 @@ -299,42 +310,49 @@ complete For file transfers using XEP-0363 + 0.1 complete + 0.1 complete + 0.1 complete + 0.1 complete + 0.1 complete + 0.1 deprecated + 0.1 Only to fetch Avatars from other users @@ -342,78 +360,98 @@ complete + 0.1 partial + 0.1 partial + 0.3 partial + 0.3 + + + + + + complete + 0.3 complete + 0.1 complete + 0.1 complete + 0.1 complete + 0.1 complete + 0.1 partial + 0.3 complete + 0.1 complete + 0.1 partial + 0.1 @@ -421,6 +459,7 @@ 1.0 complete + 0.1 @@ -428,36 +467,49 @@ partial No support for sending + 0.3 complete + 0.1 complete + 0.1 + + + + + + partial + 0.3 complete + 0.1 partial + 0.3 partial + 0.3 @@ -465,6 +517,14 @@ complete For use with XEP-0280 + 0.1 + + + + + + partial + 0.3 @@ -479,7 +539,7 @@ partial - Not for MUCs + 0.1 @@ -487,18 +547,21 @@ complete 1.0.0 + 0.3 complete + 0.1 partial + 0.1 @@ -506,24 +569,28 @@ complete 0.3.1 + 0.3 complete + 0.1 complete + 0.1 complete + 0.1 @@ -531,6 +598,7 @@ partial Only for outgoing messages + 0.1 @@ -538,42 +606,71 @@ complete 0.3.0 + 0.1 partial + 0.1 + + + + + + complete + 0.5 partial + 0.1 complete + 0.1 complete + 0.1 complete + 0.1 complete + 0.2 + + + + + + complete + 0.4 + + + + + + complete + 0.1.1 + 0.4 @@ -581,6 +678,15 @@ partial No support for embedded thumbnails + 0.1 + + + + + + complete + 0.2.0 + 0.4 diff --git a/dino.doap.in b/dino.doap.in index 563de1d4..7a175e63 100644 --- a/dino.doap.in +++ b/dino.doap.in @@ -47,24 +47,28 @@ complete + 0.1 complete + 0.1 complete + 0.1 partial + 0.1 @@ -72,6 +76,7 @@ complete For use with XEP-0261 + 0.1 @@ -79,12 +84,14 @@ deprecated Migrating to XEP-0402 if supported by server + 0.1 complete + 0.1 @@ -92,6 +99,7 @@ partial Only for viewing avatars + 0.1 @@ -99,12 +107,14 @@ partial For use with XEP-0313 + 0.1 partial + 0.1 @@ -112,6 +122,7 @@ partial For use with XEP-0260 + 0.1 @@ -119,42 +130,49 @@ complete For file transfers using XEP-0363 + 0.1 complete + 0.1 complete + 0.1 complete + 0.1 complete + 0.1 complete + 0.1 deprecated + 0.1 Only to fetch Avatars from other users @@ -162,78 +180,98 @@ complete + 0.1 partial + 0.1 partial + 0.3 partial + 0.3 + + + + + + complete + 0.3 complete + 0.1 complete + 0.1 complete + 0.1 complete + 0.1 complete + 0.1 partial + 0.3 complete + 0.1 complete + 0.1 partial + 0.1 @@ -241,6 +279,7 @@ 1.0 complete + 0.1 @@ -248,36 +287,49 @@ partial No support for sending + 0.3 complete + 0.1 complete + 0.1 + + + + + + partial + 0.3 complete + 0.1 partial + 0.3 partial + 0.3 @@ -285,6 +337,14 @@ complete For use with XEP-0280 + 0.1 + + + + + + partial + 0.3 @@ -299,7 +359,7 @@ partial - Not for MUCs + 0.1 @@ -307,18 +367,21 @@ complete 1.0.0 + 0.3 complete + 0.1 partial + 0.1 @@ -326,24 +389,28 @@ complete 0.3.1 + 0.3 complete + 0.1 complete + 0.1 complete + 0.1 @@ -351,6 +418,7 @@ partial Only for outgoing messages + 0.1 @@ -358,42 +426,71 @@ complete 0.3.0 + 0.1 partial + 0.1 + + + + + + complete + 0.5 partial + 0.1 complete + 0.1 complete + 0.1 complete + 0.1 complete + 0.2 + + + + + + complete + 0.4 + + + + + + complete + 0.1.1 + 0.4 @@ -401,6 +498,15 @@ partial No support for embedded thumbnails + 0.1 + + + + + + complete + 0.2.0 + 0.4 diff --git a/im.dino.Dino.json b/im.dino.Dino.json new file mode 100644 index 00000000..b2344c61 --- /dev/null +++ b/im.dino.Dino.json @@ -0,0 +1,75 @@ +{ + "id": "im.dino.Dino", + "runtime": "org.gnome.Platform", + "runtime-version": "44", + "sdk": "org.gnome.Sdk", + "command": "dino", + "finish-args": [ + "--share=ipc", + "--socket=fallback-x11", + "--socket=wayland", + "--socket=pulseaudio", + "--socket=gpg-agent", + "--share=network", + "--device=all", + "--talk-name=org.freedesktop.Notifications" + ], + "modules": [ + { + "name": "libsignal-protocol-c", + "buildsystem": "cmake-ninja", + "config-opts": [ + "-DCMAKE_C_FLAGS=-fPIC" + ], + "cleanup": [ + "/include", + "/lib" + ], + "sources": [ + { + "type": "git", + "url": "https://github.com/mar-v-in/libsignal-protocol-c.git", + "tag": "v2.3.3.1" + } + ] + }, + { + "name": "qrencode", + "buildsystem": "cmake-ninja", + "cleanup": [ + "/bin", + "/include", + "/lib", + "/share/man" + ], + "config-opts": [ + "-DCMAKE_C_FLAGS=-fPIC" + ], + "sources": [ + { + "type": "archive", + "url": "https://fukuchi.org/works/qrencode/qrencode-4.1.1.tar.gz", + "sha512": "209bb656ae3f391b03c7b3ceb03e34f7320b0105babf48b619e7a299528b8828449e0e7696f0b5db0d99170a81709d0518e34835229a748701e7df784e58a9ce" + } + ] + }, + { + "name": "dino", + "buildsystem": "cmake-ninja", + "builddir": true, + "config-opts": [ + "-DSOUP_VERSION=3" + ], + "cleanup": [ + "/include", + "/share/vala" + ], + "sources": [ + { + "type": "dir", + "path": "." + } + ] + } + ] +} \ No newline at end of file diff --git a/libdino/CMakeLists.txt b/libdino/CMakeLists.txt index 5aa4035f..3c184cfd 100644 --- a/libdino/CMakeLists.txt +++ b/libdino/CMakeLists.txt @@ -9,6 +9,7 @@ find_packages(LIBDINO_PACKAGES REQUIRED vala_precompile(LIBDINO_VALA_C SOURCES src/application.vala + src/version.vala src/dbus/login1.vala src/dbus/notifications.vala diff --git a/libdino/meson.build b/libdino/meson.build new file mode 100644 index 00000000..0ebaff33 --- /dev/null +++ b/libdino/meson.build @@ -0,0 +1,86 @@ +# version_vala +dot_git = meson.current_source_dir() / '../.git' +version_file = meson.current_source_dir() / '../VERSION' +command = [prog_python, files('version.py'), version_file, '@OUTPUT@', '--git-repo', meson.current_source_dir()] +if prog_git.found() + command += ['--git', prog_git] +endif +depend_files = [] +if fs.exists(dot_git) + depend_files += [dot_git] +endif +if fs.exists(version_file) + depend_files += [version_file] +endif +version_vala = custom_target('libdino_version_vala', command: command, output: 'version.vala', depend_files: depend_files) + +# libdino +dependencies = [ + dep_gdk_pixbuf, + dep_gee, + dep_gio, + dep_glib, + dep_gmodule, + dep_qlite, + dep_xmpp_vala +] +sources = files( + 'src/application.vala', + 'src/dbus/login1.vala', + 'src/dbus/notifications.vala', + 'src/dbus/upower.vala', + 'src/entity/account.vala', + 'src/entity/call.vala', + 'src/entity/conversation.vala', + 'src/entity/encryption.vala', + 'src/entity/file_transfer.vala', + 'src/entity/message.vala', + 'src/entity/settings.vala', + 'src/plugin/interfaces.vala', + 'src/plugin/loader.vala', + 'src/plugin/registry.vala', + 'src/service/avatar_manager.vala', + 'src/service/blocking_manager.vala', + 'src/service/call_store.vala', + 'src/service/call_state.vala', + 'src/service/call_peer_state.vala', + 'src/service/calls.vala', + 'src/service/chat_interaction.vala', + 'src/service/connection_manager.vala', + 'src/service/content_item_store.vala', + 'src/service/conversation_manager.vala', + 'src/service/counterpart_interaction_manager.vala', + 'src/service/database.vala', + 'src/service/entity_capabilities_storage.vala', + 'src/service/entity_info.vala', + 'src/service/fallback_body.vala', + 'src/service/file_manager.vala', + 'src/service/file_transfer_storage.vala', + 'src/service/history_sync.vala', + 'src/service/jingle_file_transfers.vala', + 'src/service/message_correction.vala', + 'src/service/message_processor.vala', + 'src/service/message_storage.vala', + 'src/service/module_manager.vala', + 'src/service/muc_manager.vala', + 'src/service/notification_events.vala', + 'src/service/presence_manager.vala', + 'src/service/replies.vala', + 'src/service/reactions.vala', + 'src/service/registration.vala', + 'src/service/roster_manager.vala', + 'src/service/search_processor.vala', + 'src/service/stream_interactor.vala', + 'src/service/util.vala', + 'src/util/display_name.vala', + 'src/util/util.vala', + 'src/util/weak_map.vala', +) +sources += [version_vala] +c_args = [ + '-DDINO_SYSTEM_LIBDIR_NAME="@0@"'.format(get_option('prefix') / get_option('libdir')), + '-DDINO_SYSTEM_PLUGIN_DIR="@0@"'.format(get_option('prefix') / get_option('plugindir')), + '-DG_LOG_DOMAIN="libdino"', +] +lib_dino = library('dino', sources, c_args: c_args, include_directories: include_directories('src'), dependencies: dependencies) +dep_dino = declare_dependency(link_with: lib_dino, include_directories: include_directories('.', 'src')) diff --git a/libdino/src/application.vala b/libdino/src/application.vala index 490cd40c..5e58e364 100644 --- a/libdino/src/application.vala +++ b/libdino/src/application.vala @@ -2,7 +2,6 @@ using Dino.Entities; namespace Dino { -extern const string VERSION; public string get_version() { return VERSION; } public string get_short_version() { if (!VERSION.contains("~")) return VERSION; diff --git a/libdino/src/plugin/interfaces.vala b/libdino/src/plugin/interfaces.vala index a73cb5f7..cfe4d0cb 100644 --- a/libdino/src/plugin/interfaces.vala +++ b/libdino/src/plugin/interfaces.vala @@ -151,8 +151,9 @@ public interface ConversationItemWidgetInterface: Object { public abstract void set_widget(Object object, WidgetType type, int priority); } -public delegate void MessageActionEvoked(Object button, Plugins.MetaConversationItem evoked_on, Object widget); +public delegate void MessageActionEvoked(Variant? variant); public class MessageAction : Object { + public string name; public bool sensitive = true; public string icon_name; public string? tooltip; diff --git a/libdino/src/plugin/registry.vala b/libdino/src/plugin/registry.vala index e2801508..6c0234ca 100644 --- a/libdino/src/plugin/registry.vala +++ b/libdino/src/plugin/registry.vala @@ -3,14 +3,14 @@ using Gee; namespace Dino.Plugins { public class Registry { - internal HashMap encryption_list_entries = new HashMap(); - internal HashMap call_encryption_entries = new HashMap(); - internal ArrayList account_settings_entries = new ArrayList(); - internal ArrayList contact_details_entries = new ArrayList(); - internal Map text_commands = new HashMap(); - internal Gee.List conversation_addition_populators = new ArrayList(); - internal Gee.List notification_populators = new ArrayList(); - internal Gee.Collection conversation_titlebar_entries = new Gee.TreeSet((a, b) => { + public HashMap encryption_list_entries = new HashMap(); + public HashMap call_encryption_entries = new HashMap(); + public ArrayList account_settings_entries = new ArrayList(); + public ArrayList contact_details_entries = new ArrayList(); + public Map text_commands = new HashMap(); + public Gee.List conversation_addition_populators = new ArrayList(); + public Gee.List notification_populators = new ArrayList(); + public Gee.Collection conversation_titlebar_entries = new Gee.TreeSet((a, b) => { return (int)(a.order - b.order); }); public VideoCallPlugin? video_call_plugin; diff --git a/libdino/src/service/avatar_manager.vala b/libdino/src/service/avatar_manager.vala index 6b44b6ea..c083bb2b 100644 --- a/libdino/src/service/avatar_manager.vala +++ b/libdino/src/service/avatar_manager.vala @@ -12,6 +12,7 @@ public class AvatarManager : StreamInteractionModule, Object { public string id { get { return IDENTITY.id; } } public signal void received_avatar(Jid jid, Account account); + public signal void fetched_avatar(Jid jid, Account account); private enum Source { USER_AVATARS, @@ -25,6 +26,7 @@ public class AvatarManager : StreamInteractionModule, Object { private HashMap vcard_avatars = new HashMap(Jid.hash_func, Jid.equals_func); private HashMap cached_pixbuf = new HashMap(); private HashMap> pending_pixbuf = new HashMap>(); + private HashSet pending_fetch = new HashSet(); private const int MAX_PIXEL = 192; public static void start(StreamInteractor stream_interactor, Database db) { @@ -45,6 +47,18 @@ public class AvatarManager : StreamInteractionModule, Object { }); } + public File? get_avatar_file(Account account, Jid jid_) { + string? hash = get_avatar_hash(account, jid_); + if (hash == null) return null; + File file = File.new_for_path(Path.build_filename(folder, hash)); + if (!file.query_exists()) { + fetch_and_store_for_jid(account, jid_); + return null; + } else { + return file; + } + } + private string? get_avatar_hash(Account account, Jid jid_) { Jid jid = jid_; if (!stream_interactor.get_module(MucManager.IDENTITY).is_groupchat_occupant(jid_, account)) { @@ -59,6 +73,7 @@ public class AvatarManager : StreamInteractionModule, Object { } } + [Version (deprecated = true)] public bool has_avatar_cached(Account account, Jid jid) { string? hash = get_avatar_hash(account, jid); return hash != null && cached_pixbuf.has_key(hash); @@ -68,6 +83,7 @@ public class AvatarManager : StreamInteractionModule, Object { return get_avatar_hash(account, jid) != null; } + [Version (deprecated = true)] public Pixbuf? get_cached_avatar(Account account, Jid jid_) { string? hash = get_avatar_hash(account, jid_); if (hash == null) return null; @@ -75,6 +91,7 @@ public class AvatarManager : StreamInteractionModule, Object { return null; } + [Version (deprecated = true)] public async Pixbuf? get_avatar(Account account, Jid jid_) { Jid jid = jid_; if (!stream_interactor.get_module(MucManager.IDENTITY).is_groupchat_occupant(jid_, account)) { @@ -111,17 +128,7 @@ public class AvatarManager : StreamInteractionModule, Object { if (image != null) { cached_pixbuf[hash] = image; } else { - Bytes? bytes = null; - if (source == 1) { - bytes = yield Xmpp.Xep.UserAvatars.fetch_image(stream, jid, hash); - } else if (source == 2) { - bytes = yield Xmpp.Xep.VCard.fetch_image(stream, jid, hash); - if (bytes == null && jid.is_bare()) { - db.avatar.delete().with(db.avatar.jid_id, "=", db.get_jid_id(jid)).perform(); - } - } - if (bytes != null) { - store_image(hash, bytes); + if (yield fetch_and_store(stream, account, jid, source, hash)) { image = yield get_image(hash); } cached_pixbuf[hash] = image; @@ -162,7 +169,7 @@ public class AvatarManager : StreamInteractionModule, Object { ); foreach (var entry in get_avatar_hashes(account, Source.USER_AVATARS).entries) { - user_avatars[entry.key] = entry.value; + on_user_avatar_received(account, entry.key, entry.value); } foreach (var entry in get_avatar_hashes(account, Source.VCARD).entries) { @@ -172,7 +179,7 @@ public class AvatarManager : StreamInteractionModule, Object { continue; } - vcard_avatars[entry.key] = entry.value; + on_vcard_avatar_received(account, entry.key, entry.value); } } @@ -218,12 +225,53 @@ public class AvatarManager : StreamInteractionModule, Object { return ret; } - public void store_image(string id, Bytes data) { + public async bool fetch_and_store_for_jid(Account account, Jid jid) { + int source = -1; + string? hash = null; + if (user_avatars.has_key(jid)) { + hash = user_avatars[jid]; + source = 1; + } else if (vcard_avatars.has_key(jid)) { + hash = vcard_avatars[jid]; + source = 2; + } else { + return false; + } + + XmppStream? stream = stream_interactor.get_stream(account); + if (stream == null || !stream.negotiation_complete) return false; + + return yield fetch_and_store(stream, account, jid, source, hash); + } + + private async bool fetch_and_store(XmppStream stream, Account account, Jid jid, int source, string? hash) { + if (hash == null || pending_fetch.contains(hash)) return false; + + pending_fetch.add(hash); + Bytes? bytes = null; + if (source == 1) { + bytes = yield Xmpp.Xep.UserAvatars.fetch_image(stream, jid, hash); + } else if (source == 2) { + bytes = yield Xmpp.Xep.VCard.fetch_image(stream, jid, hash); + if (bytes == null && jid.is_bare()) { + db.avatar.delete().with(db.avatar.jid_id, "=", db.get_jid_id(jid)).perform(); + } + } + + if (bytes != null) { + yield store_image(hash, bytes); + fetched_avatar(jid, account); + } + pending_fetch.remove(hash); + return bytes != null; + } + + private async void store_image(string id, Bytes data) { File file = File.new_for_path(Path.build_filename(folder, id)); try { if (file.query_exists()) file.delete(); //TODO y? DataOutputStream fos = new DataOutputStream(file.create(FileCreateFlags.REPLACE_DESTINATION)); - fos.write_bytes_async.begin(data); + yield fos.write_bytes_async(data); } catch (Error e) { // Ignore: we failed in storing, so we refuse to display later... } diff --git a/libdino/src/service/database.vala b/libdino/src/service/database.vala index 96b3b82d..6b3f5e6a 100644 --- a/libdino/src/service/database.vala +++ b/libdino/src/service/database.vala @@ -7,7 +7,7 @@ using Dino.Entities; namespace Dino { public class Database : Qlite.Database { - private const int VERSION = 25; + private const int VERSION = 26; public class AccountTable : Table { public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true }; @@ -93,6 +93,11 @@ public class Database : Qlite.Database { // deduplication index("message_account_counterpart_stanzaid_idx", {account_id, counterpart_id, stanza_id}); + index("message_account_counterpart_serverid_idx", {account_id, counterpart_id, server_id}); + + // message by marked + index("message_account_marked_idx", {account_id, marked}); + fts({body}); } } diff --git a/libdino/src/service/file_manager.vala b/libdino/src/service/file_manager.vala index 9cbc57cc..4073fe40 100644 --- a/libdino/src/service/file_manager.vala +++ b/libdino/src/service/file_manager.vala @@ -283,8 +283,13 @@ public class FileManager : StreamInteractionModule, Object { file_transfer.ourpart = stream_interactor.get_module(MucManager.IDENTITY).get_own_jid(conversation.counterpart, conversation.account) ?? conversation.account.bare_jid; file_transfer.direction = from.equals(file_transfer.ourpart) ? FileTransfer.DIRECTION_SENT : FileTransfer.DIRECTION_RECEIVED; } else { - file_transfer.ourpart = conversation.account.full_jid; - file_transfer.direction = from.equals_bare(file_transfer.ourpart) ? FileTransfer.DIRECTION_SENT : FileTransfer.DIRECTION_RECEIVED; + if (from.equals_bare(conversation.account.bare_jid)) { + file_transfer.ourpart = from; + file_transfer.direction = FileTransfer.DIRECTION_SENT; + } else { + file_transfer.ourpart = conversation.account.full_jid; + file_transfer.direction = FileTransfer.DIRECTION_RECEIVED; + } } file_transfer.time = time; file_transfer.local_time = local_time; diff --git a/libdino/src/service/history_sync.vala b/libdino/src/service/history_sync.vala index e83b3cb4..2444a133 100644 --- a/libdino/src/service/history_sync.vala +++ b/libdino/src/service/history_sync.vala @@ -163,7 +163,7 @@ public class Dino.HistorySync { if (current_row[db.mam_catchup.from_end]) return; debug("[%s] Fetching between ranges %s - %s", mam_server.to_string(), previous_row[db.mam_catchup.to_time].to_string(), current_row[db.mam_catchup.from_time].to_string()); - current_row = yield fetch_between_ranges(account, mam_server, previous_row, current_row); + current_row = yield fetch_between_ranges(account, mam_server, previous_row, current_row, cancellable); if (current_row == null) return; RowOption previous_row_opt = db.mam_catchup.select() @@ -214,13 +214,11 @@ public class Dino.HistorySync { return null; } - // If we get PageResult.Duplicate, we still want to update the db row to the latest message. - // Catchup finished within first page. Update latest db entry. if (latest_row_id != -1 && - page_result.page_result in new PageResult[] { PageResult.TargetReached, PageResult.NoMoreMessages, PageResult.Duplicate }) { + page_result.page_result in new PageResult[] { PageResult.TargetReached, PageResult.NoMoreMessages }) { - if (page_result.stanzas == null || page_result.stanzas.is_empty) return null; + if (page_result.stanzas == null) return null; string latest_mam_id = page_result.query_result.last; long latest_mam_time = (long) mam_times[account][latest_mam_id].to_unix(); @@ -272,7 +270,7 @@ public class Dino.HistorySync { ** Merges the `earlier_range` db row into the `later_range` db row. ** @return The resulting range comprising `earlier_range`, `later_rage`, and everything in between. null if fetching/merge failed. **/ - private async Row? fetch_between_ranges(Account account, Jid mam_server, Row earlier_range, Row later_range) { + private async Row? fetch_between_ranges(Account account, Jid mam_server, Row earlier_range, Row later_range, Cancellable? cancellable = null) { int later_range_id = (int) later_range[db.mam_catchup.id]; DateTime earliest_time = new DateTime.from_unix_utc(earlier_range[db.mam_catchup.to_time]); DateTime latest_time = new DateTime.from_unix_utc(later_range[db.mam_catchup.from_time]); @@ -282,9 +280,9 @@ public class Dino.HistorySync { earliest_time, earlier_range[db.mam_catchup.to_id], latest_time, later_range[db.mam_catchup.from_id]); - PageRequestResult page_result = yield fetch_query(account, query_params, later_range_id); + PageRequestResult page_result = yield fetch_query(account, query_params, later_range_id, cancellable); - if (page_result.page_result == PageResult.TargetReached) { + if (page_result.page_result == PageResult.TargetReached || page_result.page_result == PageResult.NoMoreMessages) { debug("[%s | %s] Merging range %i into %i", account.bare_jid.to_string(), mam_server.to_string(), earlier_range[db.mam_catchup.id], later_range_id); // Merge earlier range into later one. db.mam_catchup.update() @@ -330,9 +328,9 @@ public class Dino.HistorySync { PageRequestResult? page_result = null; do { page_result = yield get_mam_page(account, query_params, page_result, cancellable); - debug("Page result %s %b", page_result.page_result.to_string(), page_result.stanzas == null); + debug("[%s | %s] Page result %s (got stanzas: %s)", account.bare_jid.to_string(), query_params.mam_server.to_string(), page_result.page_result.to_string(), (page_result.stanzas != null).to_string()); - if (page_result.page_result == PageResult.Error || page_result.page_result == PageResult.Cancelled || page_result.stanzas == null) return page_result; + if (page_result.page_result == PageResult.Error || page_result.page_result == PageResult.Cancelled || page_result.query_result.first == null) return page_result; string earliest_mam_id = page_result.query_result.first; long earliest_mam_time = (long)mam_times[account][earliest_mam_id].to_unix(); @@ -357,7 +355,6 @@ public class Dino.HistorySync { MorePagesAvailable, TargetReached, NoMoreMessages, - Duplicate, Error, Cancelled } @@ -399,23 +396,25 @@ public class Dino.HistorySync { string query_id = query_params.query_id; string? after_id = query_params.start_id; + var stanzas_for_query = stanzas.has_key(query_id) && !stanzas[query_id].is_empty ? stanzas[query_id] : null; if (cancellable != null && cancellable.is_cancelled()) { - return new PageRequestResult(PageResult.Cancelled, query_result, stanzas[query_id]); + stanzas.unset(query_id); + return new PageRequestResult(PageResult.Cancelled, query_result, stanzas_for_query); } - if (stanzas.has_key(query_id) && !stanzas[query_id].is_empty) { + if (stanzas_for_query != null) { // Check it we reached our target (from_id) - foreach (Xmpp.MessageStanza message in stanzas[query_id]) { + foreach (Xmpp.MessageStanza message in stanzas_for_query) { Xmpp.MessageArchiveManagement.MessageFlag? mam_message_flag = Xmpp.MessageArchiveManagement.MessageFlag.get_flag(message); if (mam_message_flag != null && mam_message_flag.mam_id != null) { if (after_id != null && mam_message_flag.mam_id == after_id) { // Successfully fetched the whole range yield send_messages_back_into_pipeline(account, query_id, cancellable); if (cancellable != null && cancellable.is_cancelled()) { - return new PageRequestResult(PageResult.Cancelled, query_result, stanzas[query_id]); + return new PageRequestResult(PageResult.Cancelled, query_result, stanzas_for_query); } - return new PageRequestResult(PageResult.TargetReached, query_result, stanzas[query_id]); + return new PageRequestResult(PageResult.TargetReached, query_result, stanzas_for_query); } } } @@ -423,37 +422,9 @@ public class Dino.HistorySync { // Message got filtered out by xmpp-vala, but succesful range fetch nevertheless yield send_messages_back_into_pipeline(account, query_id); if (cancellable != null && cancellable.is_cancelled()) { - return new PageRequestResult(PageResult.Cancelled, query_result, stanzas[query_id]); + return new PageRequestResult(PageResult.Cancelled, query_result, stanzas_for_query); } - return new PageRequestResult(PageResult.TargetReached, query_result, stanzas[query_id]); - } - - // Check for duplicates. Go through all messages and build a db query. - foreach (Xmpp.MessageStanza message in stanzas[query_id]) { - Xmpp.MessageArchiveManagement.MessageFlag? mam_message_flag = Xmpp.MessageArchiveManagement.MessageFlag.get_flag(message); - if (mam_message_flag != null && mam_message_flag.mam_id != null) { - if (selection == null) { - selection = @"$(db.message.server_id) = ?"; - } else { - selection += @" OR $(db.message.server_id) = ?"; - } - selection_args += mam_message_flag.mam_id; - } - } - var duplicates_qry = db.message.select() - .with(db.message.account_id, "=", account.id) - .where(selection, selection_args); - // We don't want messages from different MAM servers to interfere with each other. - if (!query_params.mam_server.equals_bare(account.bare_jid)) { - duplicates_qry.with(db.message.counterpart_id, "=", db.get_jid_id(query_params.mam_server)); - } else { - duplicates_qry.with(db.message.type_, "=", Message.Type.CHAT); - } - var duplicates_count = duplicates_qry.count(); - if (duplicates_count > 0) { - // We got a duplicate although we thought we have to catch up. - // There was a server bug where prosody would send all messages if it didn't know the after ID that was given - page_result = PageResult.Duplicate; + return new PageRequestResult(PageResult.TargetReached, query_result, stanzas_for_query); } } @@ -461,7 +432,7 @@ public class Dino.HistorySync { if (cancellable != null && cancellable.is_cancelled()) { page_result = PageResult.Cancelled; } - return new PageRequestResult(page_result, query_result, stanzas.has_key(query_id) ? stanzas[query_id] : null); + return new PageRequestResult(page_result, query_result, stanzas_for_query); } private async void send_messages_back_into_pipeline(Account account, string query_id, Cancellable? cancellable = null) { diff --git a/libdino/src/service/message_processor.vala b/libdino/src/service/message_processor.vala index 12bbeeac..45f06a69 100644 --- a/libdino/src/service/message_processor.vala +++ b/libdino/src/service/message_processor.vala @@ -451,6 +451,10 @@ public class MessageProcessor : StreamInteractionModule, Object { } } + if (conversation.get_send_typing_setting(stream_interactor) == Conversation.Setting.ON) { + ChatStateNotifications.add_state_to_message(new_message, ChatStateNotifications.STATE_ACTIVE); + } + stream.get_module(MessageModule.IDENTITY).send_message.begin(stream, new_message, (_, res) => { try { stream.get_module(MessageModule.IDENTITY).send_message.end(res); diff --git a/libdino/src/version.vala b/libdino/src/version.vala new file mode 100644 index 00000000..0fdc0145 --- /dev/null +++ b/libdino/src/version.vala @@ -0,0 +1,6 @@ +// Not used in Meson. +namespace Dino { + +extern const string VERSION; + +} diff --git a/libdino/version.py b/libdino/version.py new file mode 100644 index 00000000..d34db6a8 --- /dev/null +++ b/libdino/version.py @@ -0,0 +1,36 @@ +import argparse +import subprocess +VERSION_VALA = """\ +namespace Dino {{ + +public const string VERSION = "{}"; + +}} +""" + +def compute_version(file, git_repo, git): + try: + with open(file) as f: + return f.read().strip() + except FileNotFoundError: + pass + return subprocess.check_output([git, "describe", "--tags"], cwd=git_repo, text=True).strip() + +def generate_version_vala(version): + if "\\" in version or "\"" in version: + raise ValueError(f"invalid version {version!r}") + return VERSION_VALA.format(version) + +def main(): + p = argparse.ArgumentParser(description="Compute the Dino version") + p.add_argument("--git-repo", help="Path to checked out git repository") + p.add_argument("--git", help="Path to git executable", default="git") + p.add_argument("version_file", metavar="VERSION_FILE", help="Use this file's contents as version if the file exists") + p.add_argument("output", metavar="OUTPUT", help="Vala file to output to") + args = p.parse_args() + out = generate_version_vala(compute_version(args.version_file, args.git_repo, args.git)) + with open(args.output, "w") as f: + f.write(out) + +if __name__ == "__main__": + main() diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 127059d5..9a48bb4e 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -124,8 +124,6 @@ SOURCES src/main.vala src/ui/application.vala - src/ui/avatar_drawer.vala - src/ui/avatar_image.vala src/ui/conversation_list_titlebar.vala src/ui/conversation_view.vala src/ui/conversation_view_controller.vala @@ -209,6 +207,7 @@ SOURCES src/ui/util/sizing_bin.vala src/ui/util/size_request_box.vala + src/ui/widgets/avatar_picture.vala src/ui/widgets/date_separator.vala src/ui/widgets/fixed_ratio_picture.vala src/ui/widgets/natural_size_increase.vala @@ -238,6 +237,7 @@ endif(WIN32) add_dependencies(dino ${GETTEXT_PACKAGE}-translations) target_include_directories(dino PRIVATE src) target_link_libraries(dino libdino ${MAIN_PACKAGES}) +set_target_properties(dino PROPERTIES ENABLE_EXPORTS TRUE) if(WIN32) target_link_libraries(dino -mwindows) diff --git a/main/data/add_conversation/list_row.ui b/main/data/add_conversation/list_row.ui index c0d7e517..b8a97174 100644 --- a/main/data/add_conversation/list_row.ui +++ b/main/data/add_conversation/list_row.ui @@ -8,10 +8,9 @@ 3 10 - - False - 30 - 30 + + 30 + 30 center diff --git a/main/data/contact_details_dialog.ui b/main/data/contact_details_dialog.ui index 64a0e5fc..4802ae9a 100644 --- a/main/data/contact_details_dialog.ui +++ b/main/data/contact_details_dialog.ui @@ -30,10 +30,10 @@ 100 10 - - 50 - 50 - False + + 50 + 50 + center 0 0 diff --git a/main/data/conversation_item_widget.ui b/main/data/conversation_item_widget.ui index cd6c9269..3216232d 100644 --- a/main/data/conversation_item_widget.ui +++ b/main/data/conversation_item_widget.ui @@ -5,9 +5,9 @@ 7 2 - - 35 - 35 + + 35 + 35 start 2 diff --git a/main/data/conversation_row.ui b/main/data/conversation_row.ui index 2eb9071b..3bd5527d 100644 --- a/main/data/conversation_row.ui +++ b/main/data/conversation_row.ui @@ -14,9 +14,9 @@ 7 14 - - 35 - 35 + + 35 + 35 center diff --git a/main/data/gresource.xml b/main/data/gresource.xml new file mode 100644 index 00000000..6d9febab --- /dev/null +++ b/main/data/gresource.xml @@ -0,0 +1,74 @@ + + + + add_conversation/add_contact_dialog.ui + add_conversation/add_groupchat_dialog.ui + add_conversation/conference_details_fragment.ui + add_conversation/list_row.ui + add_conversation/select_jid_fragment.ui + call_widget.ui + chat_input.ui + contact_details_dialog.ui + conversation_content_view/item_metadata_header.ui + conversation_content_view/view.ui + conversation_item_widget.ui + conversation_list_titlebar.ui + conversation_list_titlebar_csd.ui + conversation_row.ui + conversation_view.ui + dino-conversation-list-placeholder-arrow.svg + file_default_widget.ui + file_send_overlay.ui + global_search.ui + icons/scalable/actions/dino-account-plus-symbolic.svg + icons/scalable/actions/dino-emoticon-add-symbolic.svg + icons/scalable/actions/dino-emoticon-symbolic.svg + icons/scalable/actions/dino-qr-code-symbolic.svg + icons/scalable/apps/im.dino.Dino-symbolic.svg + icons/scalable/apps/im.dino.Dino.svg + icons/scalable/devices/dino-device-desktop-symbolic.svg + icons/scalable/devices/dino-device-phone-symbolic.svg + icons/scalable/devices/dino-phone-hangup-symbolic.svg + icons/scalable/devices/dino-phone-in-talk-symbolic.svg + icons/scalable/devices/dino-phone-missed-symbolic.svg + icons/scalable/devices/dino-phone-ring-symbolic.svg + icons/scalable/devices/dino-phone-symbolic.svg + icons/scalable/mimetypes/dino-file-document-symbolic.svg + icons/scalable/mimetypes/dino-file-download-symbolic.svg + icons/scalable/mimetypes/dino-file-image-symbolic.svg + icons/scalable/mimetypes/dino-file-music-symbolic.svg + icons/scalable/mimetypes/dino-file-symbolic.svg + icons/scalable/mimetypes/dino-file-table-symbolic.svg + icons/scalable/mimetypes/dino-file-video-symbolic.svg + icons/scalable/status/dino-double-tick-symbolic.svg + icons/scalable/status/dino-microphone-off-symbolic.svg + icons/scalable/status/dino-microphone-symbolic.svg + icons/scalable/status/dino-party-popper-symbolic.svg + icons/scalable/status/dino-security-high-symbolic.svg + icons/scalable/status/dino-status-away.svg + icons/scalable/status/dino-status-chat.svg + icons/scalable/status/dino-status-dnd.svg + icons/scalable/status/dino-status-online.svg + icons/scalable/status/dino-tick-symbolic.svg + icons/scalable/status/dino-video-off-symbolic.svg + icons/scalable/status/dino-video-symbolic.svg + manage_accounts/account_row.ui + manage_accounts/add_account_dialog.ui + manage_accounts/dialog.ui + menu_add.ui + menu_app.ui + menu_conversation.ui + menu_encryption.ui + message_item_widget_edit_mode.ui + occupant_list.ui + occupant_list_item.ui + quote.ui + search_autocomplete.ui + settings_dialog.ui + shortcuts.ui + style-dark.css + style.css + unified_main_content.ui + unified_window_placeholder.ui + + diff --git a/main/data/im.dino.Dino.desktop b/main/data/im.dino.Dino.desktop index 90f3e38f..8c04dc34 100644 --- a/main/data/im.dino.Dino.desktop +++ b/main/data/im.dino.Dino.desktop @@ -5,7 +5,7 @@ GenericName=Jabber/XMPP Client Keywords=chat;talk;im;message;xmpp;jabber; Exec=dino %U Icon=im.dino.Dino -StartupNotify=false +StartupNotify=true Terminal=false Type=Application Categories=GTK;Network;Chat;InstantMessaging; diff --git a/main/data/manage_accounts/account_row.ui b/main/data/manage_accounts/account_row.ui index 845010a2..91891b91 100644 --- a/main/data/manage_accounts/account_row.ui +++ b/main/data/manage_accounts/account_row.ui @@ -10,9 +10,9 @@ 6 6 - - 40 - 40 + + 40 + 40 diff --git a/main/data/manage_accounts/dialog.ui b/main/data/manage_accounts/dialog.ui index 90a36b83..4931507c 100644 --- a/main/data/manage_accounts/dialog.ui +++ b/main/data/manage_accounts/dialog.ui @@ -93,11 +93,9 @@ - - 50 - 50 - - False + + 50 + 50 diff --git a/main/data/occupant_list_item.ui b/main/data/occupant_list_item.ui index 1915aee6..47e63bc9 100644 --- a/main/data/occupant_list_item.ui +++ b/main/data/occupant_list_item.ui @@ -8,9 +8,9 @@ 7 10 - - 30 - 30 + + 30 + 30 diff --git a/main/data/quote.ui b/main/data/quote.ui index a7c32ed8..277fc374 100644 --- a/main/data/quote.ui +++ b/main/data/quote.ui @@ -7,10 +7,9 @@ - - False - 15 - 15 + + 15 + 15 center 0 diff --git a/main/data/search_autocomplete.ui b/main/data/search_autocomplete.ui index a63bdce9..d607b192 100644 --- a/main/data/search_autocomplete.ui +++ b/main/data/search_autocomplete.ui @@ -3,14 +3,13 @@ horizontal - + 4 4 6 6 - 24 - 24 - False + 24 + 24 diff --git a/main/data/style.css b/main/data/style.css index fffee8a3..deac24fe 100644 --- a/main/data/style.css +++ b/main/data/style.css @@ -31,8 +31,8 @@ window.dino-main .dino-conversation viewport /* Some themes set this */ { } @keyframes highlight { - from { background: alpha(@warning_color, 0.5); } - to { background: transparent; } + from { background-color: alpha(@accent_color, 0.5); } + to { background-color: transparent; } } window.dino-main .dino-conversation .highlight-once { @@ -42,7 +42,7 @@ window.dino-main .dino-conversation .highlight-once { animation-name: highlight; } -window.dino-main .dino-conversation .message-box.highlight { +window.dino-main .dino-conversation .message-box.highlight:not(.highlight-once) { background: @window_bg_color; } @@ -119,6 +119,10 @@ window.dino-main .dino-quote:hover { background: alpha(@theme_fg_color, 0.08); } +picture.avatar { + border-radius: 3px; +} + /* Overlay Toolbar */ .dino-main .overlay-toolbar { diff --git a/main/meson.build b/main/meson.build new file mode 100644 index 00000000..a38e15b8 --- /dev/null +++ b/main/meson.build @@ -0,0 +1,104 @@ +dependencies = [ + dep_dino, + dep_gee, + dep_glib, + dep_gmodule, + dep_gtk4, + dep_icu_uc, + dep_libadwaita, + dep_m, + dep_qlite, + dep_xmpp_vala, +] +sources = files( + 'src/main.vala', + 'src/ui/add_conversation/add_conference_dialog.vala', + 'src/ui/add_conversation/add_contact_dialog.vala', + 'src/ui/add_conversation/add_groupchat_dialog.vala', + 'src/ui/add_conversation/conference_details_fragment.vala', + 'src/ui/add_conversation/conference_list.vala', + 'src/ui/add_conversation/list_row.vala', + 'src/ui/add_conversation/roster_list.vala', + 'src/ui/add_conversation/select_contact_dialog.vala', + 'src/ui/add_conversation/select_jid_fragment.vala', + 'src/ui/application.vala', + 'src/ui/call_window/audio_settings_popover.vala', + 'src/ui/call_window/call_bottom_bar.vala', + 'src/ui/call_window/call_connection_details_window.vala', + 'src/ui/call_window/call_encryption_button.vala', + 'src/ui/call_window/call_window.vala', + 'src/ui/call_window/call_window_controller.vala', + 'src/ui/call_window/participant_widget.vala', + 'src/ui/call_window/video_settings_popover.vala', + 'src/ui/chat_input/chat_input_controller.vala', + 'src/ui/chat_input/chat_text_view.vala', + 'src/ui/chat_input/encryption_button.vala', + 'src/ui/chat_input/occupants_tab_completer.vala', + 'src/ui/chat_input/smiley_converter.vala', + 'src/ui/chat_input/view.vala', + 'src/ui/contact_details/blocking_provider.vala', + 'src/ui/contact_details/dialog.vala', + 'src/ui/contact_details/muc_config_form_provider.vala', + 'src/ui/contact_details/permissions_provider.vala', + 'src/ui/contact_details/settings_provider.vala', + 'src/ui/conversation_content_view/call_widget.vala', + 'src/ui/conversation_content_view/chat_state_populator.vala', + 'src/ui/conversation_content_view/content_populator.vala', + 'src/ui/conversation_content_view/conversation_item_skeleton.vala', + 'src/ui/conversation_content_view/conversation_view.vala', + 'src/ui/conversation_content_view/date_separator_populator.vala', + 'src/ui/conversation_content_view/file_default_widget.vala', + 'src/ui/conversation_content_view/file_image_widget.vala', + 'src/ui/conversation_content_view/file_widget.vala', + 'src/ui/conversation_content_view/item_actions.vala', + 'src/ui/conversation_content_view/message_widget.vala', + 'src/ui/conversation_content_view/quote_widget.vala', + 'src/ui/conversation_content_view/reactions_widget.vala', + 'src/ui/conversation_content_view/subscription_notification.vala', + 'src/ui/conversation_list_titlebar.vala', + 'src/ui/conversation_selector/conversation_selector.vala', + 'src/ui/conversation_selector/conversation_selector_row.vala', + 'src/ui/conversation_titlebar/call_entry.vala', + 'src/ui/conversation_titlebar/conversation_titlebar.vala', + 'src/ui/conversation_titlebar/menu_entry.vala', + 'src/ui/conversation_titlebar/occupants_entry.vala', + 'src/ui/conversation_titlebar/search_entry.vala', + 'src/ui/conversation_view.vala', + 'src/ui/conversation_view_controller.vala', + 'src/ui/file_send_overlay.vala', + 'src/ui/global_search.vala', + 'src/ui/main_window.vala', + 'src/ui/main_window_controller.vala', + 'src/ui/manage_accounts/account_row.vala', + 'src/ui/manage_accounts/add_account_dialog.vala', + 'src/ui/manage_accounts/dialog.vala', + 'src/ui/notifier_freedesktop.vala', + 'src/ui/notifier_gnotifications.vala', + 'src/ui/occupant_menu/list.vala', + 'src/ui/occupant_menu/list_row.vala', + 'src/ui/occupant_menu/view.vala', + 'src/ui/settings_dialog.vala', + 'src/ui/util/accounts_combo_box.vala', + 'src/ui/util/config.vala', + 'src/ui/util/data_forms.vala', + 'src/ui/util/helper.vala', + 'src/ui/util/label_hybrid.vala', + 'src/ui/util/size_request_box.vala', + 'src/ui/util/sizing_bin.vala', + 'src/ui/widgets/avatar_picture.vala', + 'src/ui/widgets/date_separator.vala', + 'src/ui/widgets/fixed_ratio_picture.vala', + 'src/ui/widgets/natural_size_increase.vala', +) +sources += import('gnome').compile_resources( + 'dino-resources', + 'data/gresource.xml', + source_dir: 'data', +) + +c_args = [ + '-DG_LOG_DOMAIN="dino"', + '-DGETTEXT_PACKAGE="dino"', + '-DLOCALE_INSTALL_DIR="@0@"'.format(get_option('prefix') / get_option('localedir')), +] +exe_dino = executable('dino', sources, c_args: c_args, vala_args: ['--vapidir', meson.current_source_dir() / 'vapi'], dependencies: dependencies) diff --git a/main/src/ui/add_conversation/conference_list.vala b/main/src/ui/add_conversation/conference_list.vala index 14beaf92..0b630ae4 100644 --- a/main/src/ui/add_conversation/conference_list.vala +++ b/main/src/ui/add_conversation/conference_list.vala @@ -112,7 +112,7 @@ internal class ConferenceListRow : ListRow { via_label.visible = false; } - image.set_conversation(stream_interactor, new Conversation(jid, account, Conversation.Type.GROUPCHAT)); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).set_conversation(new Conversation(jid, account, Conversation.Type.GROUPCHAT)); } } diff --git a/main/src/ui/add_conversation/list_row.vala b/main/src/ui/add_conversation/list_row.vala index 5b3ec49a..c5e344d0 100644 --- a/main/src/ui/add_conversation/list_row.vala +++ b/main/src/ui/add_conversation/list_row.vala @@ -9,7 +9,7 @@ namespace Dino.Ui { public class ListRow : Widget { public Grid outer_grid; - public AvatarImage image; + public AvatarPicture picture; public Label name_label; public Label via_label; @@ -19,7 +19,7 @@ public class ListRow : Widget { construct { Builder builder = new Builder.from_resource("/im/dino/Dino/add_conversation/list_row.ui"); outer_grid = (Grid) builder.get_object("outer_grid"); - image = (AvatarImage) builder.get_object("image"); + picture = (AvatarPicture) builder.get_object("picture"); name_label = (Label) builder.get_object("name_label"); via_label = (Label) builder.get_object("via_label"); @@ -45,7 +45,7 @@ public class ListRow : Widget { via_label.visible = false; } name_label.label = display_name; - image.set_conversation(stream_interactor, conv); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).set_conversation(conv); } public override void dispose() { diff --git a/main/src/ui/add_conversation/select_jid_fragment.vala b/main/src/ui/add_conversation/select_jid_fragment.vala index 25b0b11f..e0682e29 100644 --- a/main/src/ui/add_conversation/select_jid_fragment.vala +++ b/main/src/ui/add_conversation/select_jid_fragment.vala @@ -132,7 +132,7 @@ public class SelectJidFragment : Gtk.Box { } else { via_label.visible = false; } - image.set_text("?"); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add("+"); } } } diff --git a/main/src/ui/avatar_drawer.vala b/main/src/ui/avatar_drawer.vala deleted file mode 100644 index c14d7fda..00000000 --- a/main/src/ui/avatar_drawer.vala +++ /dev/null @@ -1,193 +0,0 @@ -using Cairo; -using Gee; -using Gdk; -using Gtk; -using Xmpp.Util; - -namespace Dino.Ui { - -public class AvatarDrawer { - public const string GRAY = "555753"; - - private Gee.List tiles = new ArrayList(); - private int height = 35; - private int width = 35; - private bool gray; - private int base_factor = 1; - private string font_family = "Sans"; - - public AvatarDrawer size(int height, int width = height) { - this.height = height; - this.width = width; - return this; - } - - public AvatarDrawer grayscale() { - this.gray = true; - return this; - } - - public AvatarDrawer tile(Pixbuf? image, string? name, string? hex_color) { - tiles.add(new AvatarTile(image, name, hex_color)); - return this; - } - - public AvatarDrawer plus() { - tiles.add(new AvatarTile(null, "…", GRAY)); - return this; - } - - public AvatarDrawer scale(int base_factor) { - this.base_factor = base_factor; - return this; - } - - public AvatarDrawer font(string font_family) { - this.font_family = font_family; - return this; - } - - public ImageSurface draw_image_surface() { - ImageSurface surface = new ImageSurface(Format.ARGB32, width, height); - draw_on_context(new Context(surface)); - return surface; - } - - public void draw_on_context(Cairo.Context ctx) { - double radius = 3 * base_factor; - double degrees = Math.PI / 180.0; - ctx.new_sub_path(); - ctx.arc(width - radius, radius, radius, -90 * degrees, 0 * degrees); - ctx.arc(width - radius, height - radius, radius, 0 * degrees, 90 * degrees); - ctx.arc(radius, height - radius, radius, 90 * degrees, 180 * degrees); - ctx.arc(radius, radius, radius, 180 * degrees, 270 * degrees); - ctx.close_path(); - ctx.clip(); - - if (this.tiles.size == 4) { - Cairo.Surface buffer = new Cairo.Surface.similar(ctx.get_target(), Cairo.Content.COLOR_ALPHA, width, height); - Cairo.Context bufctx = new Cairo.Context(buffer); - bufctx.scale(0.5, 0.5); - bufctx.set_source_surface(sub_surface_idx(ctx, 0, width - 1, height - 1, 2 * base_factor), 0, 0); - bufctx.paint(); - bufctx.set_source_surface(sub_surface_idx(ctx, 1, width - 1, height - 1, 2 * base_factor), width + 1, 0); - bufctx.paint(); - bufctx.set_source_surface(sub_surface_idx(ctx, 2, width - 1, height - 1, 2 * base_factor), 0, height + 1); - bufctx.paint(); - bufctx.set_source_surface(sub_surface_idx(ctx, 3, width - 1, height - 1, 2 * base_factor), width + 1, height + 1); - bufctx.paint(); - - ctx.set_source_surface(buffer, 0, 0); - ctx.paint(); - } else if (this.tiles.size == 3) { - Cairo.Surface buffer = new Cairo.Surface.similar(ctx.get_target(), Cairo.Content.COLOR_ALPHA, width, height); - Cairo.Context bufctx = new Cairo.Context(buffer); - bufctx.scale(0.5, 0.5); - bufctx.set_source_surface(sub_surface_idx(ctx, 0, width - 1, height - 1, 2 * base_factor), 0, 0); - bufctx.paint(); - bufctx.set_source_surface(sub_surface_idx(ctx, 1, width - 1, height * 2, 2 * base_factor), width + 1, 0); - bufctx.paint(); - bufctx.set_source_surface(sub_surface_idx(ctx, 2, width - 1, height - 1, 2 * base_factor), 0, height + 1); - bufctx.paint(); - - ctx.set_source_surface(buffer, 0, 0); - ctx.paint(); - } else if (this.tiles.size == 2) { - Cairo.Surface buffer = new Cairo.Surface.similar(ctx.get_target(), Cairo.Content.COLOR_ALPHA, width, height); - Cairo.Context bufctx = new Cairo.Context(buffer); - bufctx.scale(0.5, 0.5); - bufctx.set_source_surface(sub_surface_idx(ctx, 0, width - 1, height * 2, 2 * base_factor), 0, 0); - bufctx.paint(); - bufctx.set_source_surface(sub_surface_idx(ctx, 1, width - 1, height * 2, 2 * base_factor), width + 1, 0); - bufctx.paint(); - - ctx.set_source_surface(buffer, 0, 0); - ctx.paint(); - } else if (this.tiles.size == 1) { - ctx.set_source_surface(sub_surface_idx(ctx, 0, width, height, base_factor), 0, 0); - ctx.paint(); - } else if (this.tiles.size == 0) { - ctx.set_source_surface(sub_surface_idx(ctx, -1, width, height, base_factor), 0, 0); - ctx.paint(); - } - - if (gray) { - // convert to greyscale - ctx.set_operator(Cairo.Operator.HSL_COLOR); - ctx.set_source_rgb(1, 1, 1); - ctx.rectangle(0, 0, width, height); - ctx.fill(); - // make the visible part more light - ctx.set_operator(Cairo.Operator.ATOP); - ctx.set_source_rgba(1, 1, 1, 0.7); - ctx.rectangle(0, 0, width, height); - ctx.fill(); - } - ctx.set_source_rgb(0, 0, 0); - } - - private Cairo.Surface sub_surface_idx(Cairo.Context ctx, int idx, int width, int height, int font_factor = 1) { - Gdk.Pixbuf? avatar = idx >= 0 ? tiles[idx].image : null; - string? name = idx >= 0 ? tiles[idx].name : ""; - string hex_color = !gray && idx >= 0 ? tiles[idx].hex_color : GRAY; - return sub_surface(ctx, font_family, avatar, name, hex_color, width, height, font_factor); - } - - private static Cairo.Surface sub_surface(Cairo.Context ctx, string font_family, Gdk.Pixbuf? avatar, string? name, string? hex_color, int width, int height, int font_factor = 1) { - Cairo.Surface buffer = new Cairo.Surface.similar(ctx.get_target(), Cairo.Content.COLOR_ALPHA, width, height); - Cairo.Context bufctx = new Cairo.Context(buffer); - if (avatar == null) { - set_source_hex_color(bufctx, hex_color ?? GRAY); - bufctx.rectangle(0, 0, width, height); - bufctx.fill(); - - string text = name == null ? "…" : name.get_char(0).toupper().to_string(); - bufctx.select_font_face(font_family, Cairo.FontSlant.NORMAL, Cairo.FontWeight.NORMAL); - bufctx.set_font_size(width / font_factor < 40 ? font_factor * 17 : font_factor * 25); - Cairo.TextExtents extents; - bufctx.text_extents(text, out extents); - double x_pos = width/2 - (extents.width/2 + extents.x_bearing); - double y_pos = height/2 - (extents.height/2 + extents.y_bearing); - bufctx.move_to(x_pos, y_pos); - bufctx.set_source_rgba(1, 1, 1, 1); - bufctx.show_text(text); - } else { - double w_scale = (double) width / avatar.width; - double h_scale = (double) height / avatar.height; - double scale = double.max(w_scale, h_scale); - bufctx.scale(scale, scale); - - double x_off = 0, y_off = 0; - if (scale == h_scale) { - x_off = (width / scale - avatar.width) / 2.0; - } else { - y_off = (height / scale - avatar.height) / 2.0; - } - Gdk.cairo_set_source_pixbuf(bufctx, avatar, x_off, y_off); - bufctx.get_source().set_filter(Cairo.Filter.BEST); - bufctx.paint(); - } - return buffer; - } - - private static void set_source_hex_color(Cairo.Context ctx, string hex_color) { - ctx.set_source_rgba((double) from_hex(hex_color.substring(0, 2)) / 255, - (double) from_hex(hex_color.substring(2, 2)) / 255, - (double) from_hex(hex_color.substring(4, 2)) / 255, - hex_color.length > 6 ? (double) from_hex(hex_color.substring(6, 2)) / 255 : 1); - } -} - -private class AvatarTile { - public Pixbuf? image { get; private set; } - public string? name { get; private set; } - public string? hex_color { get; private set; } - - public AvatarTile(Pixbuf? image, string? name, string? hex_color) { - this.image = image; - this.name = name; - this.hex_color = hex_color; - } -} - -} \ No newline at end of file diff --git a/main/src/ui/avatar_generator.vala b/main/src/ui/avatar_generator.vala deleted file mode 100644 index e69de29b..00000000 diff --git a/main/src/ui/avatar_image.vala b/main/src/ui/avatar_image.vala deleted file mode 100644 index f348dd4b..00000000 --- a/main/src/ui/avatar_image.vala +++ /dev/null @@ -1,267 +0,0 @@ -using Gtk; -using Dino.Entities; -using Xmpp; -using Xmpp.Util; - -namespace Dino.Ui { - -public class AvatarImage : Widget { - public int height { get; set; default = 35; } - public int width { get; set; default = 35; } - public bool allow_gray { get; set; default = true; } - public bool force_gray { get; set; default = false; } - public StreamInteractor? stream_interactor { get; set; } - public AvatarManager? avatar_manager { owned get { return stream_interactor == null ? null : stream_interactor.get_module(AvatarManager.IDENTITY); } } - public MucManager? muc_manager { owned get { return stream_interactor == null ? null : stream_interactor.get_module(MucManager.IDENTITY); } } - public PresenceManager? presence_manager { owned get { return stream_interactor == null ? null : stream_interactor.get_module(PresenceManager.IDENTITY); } } - public ConnectionManager? connection_manager { owned get { return stream_interactor == null ? null : stream_interactor.connection_manager; } } - public Account account { get { return conversation.account; } } - private AvatarDrawer? drawer; - private Conversation conversation; - private Jid[] jids; - private Cairo.ImageSurface? cached_surface; - private static int8 use_image_surface = -1; - - public AvatarImage() { - can_focus = false; - add_css_class("avatar"); - } - - public override void dispose() { - base.dispose(); - drawer = null; - cached_surface = null; - disconnect_stream_interactor(); - } - - public override void measure(Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { - if (orientation == Orientation.HORIZONTAL) { - minimum = width; - natural = width; - } else { - minimum = height; - natural = height; - } - minimum_baseline = natural_baseline = -1; - } - - public override void snapshot(Snapshot snapshot) { - Cairo.Context context = snapshot.append_cairo(Graphene.Rect.alloc().init(0, 0, width, height)); - draw(context); - } - - public bool draw(Cairo.Context ctx_in) { - Cairo.Context ctx = ctx_in; - int width = this.width, height = this.height, base_factor = 1; - if (use_image_surface == -1) { - // TODO: detect if we have to buffer in image surface - use_image_surface = 1; - } - if (use_image_surface == 1) { - ctx_in.scale(1f / scale_factor, 1f / scale_factor); - if (cached_surface != null) { - ctx_in.set_source_surface(cached_surface, 0, 0); - ctx_in.paint(); - ctx_in.set_source_rgb(0, 0, 0); - return true; - } - width *= scale_factor; - height *= scale_factor; - base_factor *= scale_factor; - cached_surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, width, height); - ctx = new Cairo.Context(cached_surface); - } - - AvatarDrawer drawer = this.drawer; - Jid[] jids = this.jids; - if (drawer == null && jids.length == 0) { - switch (conversation.type_) { - case Conversation.Type.CHAT: - case Conversation.Type.GROUPCHAT_PM: - // In direct chats or group chats, conversation avatar is same as counterpart avatar - jids = { conversation.counterpart }; - break; - case Conversation.Type.GROUPCHAT: - string user_color = Util.get_avatar_hex_color(stream_interactor, account, conversation.counterpart, conversation); - if (avatar_manager.has_avatar_cached(account, conversation.counterpart)) { - drawer = new AvatarDrawer().tile(avatar_manager.get_cached_avatar(account, conversation.counterpart), "#", user_color); - if (force_gray || allow_gray && (!is_self_online() || !is_counterpart_online())) drawer.grayscale(); - } else { - Gee.List? occupants = muc_manager.get_other_offline_members(conversation.counterpart, account); - if (muc_manager.is_private_room(account, conversation.counterpart) && occupants != null && occupants.size > 0) { - jids = occupants.to_array(); - } else { - drawer = new AvatarDrawer().tile(null, "#", user_color); - if (force_gray || allow_gray && (!is_self_online() || !is_counterpart_online())) drawer.grayscale(); - } - try_load_avatar_async(conversation.counterpart); - } - break; - } - } - if (drawer == null && jids.length > 0) { - drawer = new AvatarDrawer(); - for (int i = 0; i < (jids.length <= 4 ? jids.length : 3); i++) { - Jid avatar_jid = jids[i]; - Jid? real_avatar_jid = null; - if (conversation.type_ != Conversation.Type.CHAT && avatar_jid.equals_bare(conversation.counterpart) && muc_manager.is_private_room(account, conversation.counterpart.bare_jid)) { - // In private room, consider real jid - real_avatar_jid = muc_manager.get_real_jid(avatar_jid, account) ?? avatar_jid; - } - string display_name = Util.get_participant_display_name(stream_interactor, conversation, jids[i]); - string user_color = Util.get_avatar_hex_color(stream_interactor, account, jids[i], conversation); - if (avatar_manager.has_avatar_cached(account, avatar_jid)) { - drawer.tile(avatar_manager.get_cached_avatar(account, avatar_jid), display_name, user_color); - } else if (real_avatar_jid != null && avatar_manager.has_avatar_cached(account, real_avatar_jid)) { - drawer.tile(avatar_manager.get_cached_avatar(account, real_avatar_jid), display_name, user_color); - } else { - drawer.tile(null, display_name, user_color); - try_load_avatar_async(avatar_jid); - if (real_avatar_jid != null) try_load_avatar_async(real_avatar_jid); - } - } - if (jids.length > 4) { - drawer.plus(); - } - if (force_gray || allow_gray && (!is_self_online() || !is_counterpart_online())) drawer.grayscale(); - } - - - if (drawer == null) return false; - drawer.size(height, width) - .scale(base_factor) - .font(get_pango_context().get_font_description().get_family()) - .draw_on_context(ctx); - - if (use_image_surface == 1) { - ctx_in.set_source_surface(ctx.get_target(), 0, 0); - ctx_in.paint(); - ctx_in.set_source_rgb(0, 0, 0); - } - - return true; - } - - private void try_load_avatar_async(Jid jid) { - if (avatar_manager.has_avatar(account, jid)) { - avatar_manager.get_avatar.begin(account, jid, (_, res) => { - var avatar = avatar_manager.get_avatar.end(res); - if (avatar != null) force_redraw(); - }); - } - } - - private void force_redraw() { - this.cached_surface = null; - queue_draw(); - } - - private void disconnect_stream_interactor() { - if (stream_interactor != null) { - presence_manager.show_received.disconnect(on_show_received); - presence_manager.received_offline_presence.disconnect(on_show_received); - avatar_manager.received_avatar.disconnect(on_received_avatar); - stream_interactor.connection_manager.connection_state_changed.disconnect(on_connection_changed); - stream_interactor.get_module(RosterManager.IDENTITY).updated_roster_item.disconnect(on_roster_updated); - muc_manager.private_room_occupant_updated.disconnect(on_private_room_occupant_updated); - muc_manager.room_info_updated.disconnect(on_room_info_updated); - stream_interactor = null; - } - } - - private void on_show_received(Jid jid, Account account) { - if (!account.equals(this.account)) return; - update_avatar_if_jid(jid); - } - - private void on_received_avatar(Jid jid, Account account) { - if (!account.equals(this.account)) return; - update_avatar_if_jid(jid); - } - - private void update_avatar_if_jid(Jid jid) { - if (jid.equals_bare(this.conversation.counterpart)) { - force_redraw(); - return; - } - foreach (Jid ours in this.jids) { - if (jid.equals_bare(ours)) { - force_redraw(); - return; - } - } - } - - private void on_connection_changed(Account account, ConnectionManager.ConnectionState state) { - if (!account.equals(this.account)) return; - force_redraw(); - } - - private void on_roster_updated(Account account, Jid jid, Roster.Item roster_item) { - if (!account.equals(this.account)) return; - update_avatar_if_jid(jid); - } - - private void on_private_room_occupant_updated(Account account, Jid room, Jid occupant) { - if (!account.equals(this.account)) return; - update_avatar_if_jid(room); - } - - private void on_room_info_updated(Account account, Jid muc_jid) { - if (!account.equals(this.account)) return; - update_avatar_if_jid(muc_jid); - } - - private bool is_self_online() { - if (connection_manager != null) { - return connection_manager.get_state(account) == ConnectionManager.ConnectionState.CONNECTED; - } - return false; - } - - private bool is_counterpart_online() { - return presence_manager.get_full_jids(conversation.counterpart, account) != null; - } - - public void set_conversation(StreamInteractor stream_interactor, Conversation conversation) { - set_avatar(stream_interactor, conversation, new Jid[0]); - } - - public void set_conversation_participant(StreamInteractor stream_interactor, Conversation conversation, Jid sub_jid) { - set_avatar(stream_interactor, conversation, new Jid[] {sub_jid}); - } - - public void set_conversation_participants(StreamInteractor stream_interactor, Conversation conversation, Jid[] sub_jids) { - set_avatar(stream_interactor, conversation, sub_jids); - } - - private void set_avatar(StreamInteractor stream_interactor, Conversation conversation, Jid[] jids) { - if (this.stream_interactor != null && stream_interactor != this.stream_interactor) { - disconnect_stream_interactor(); - } - if (this.stream_interactor != stream_interactor) { - this.stream_interactor = stream_interactor; - presence_manager.show_received.connect(on_show_received); - presence_manager.received_offline_presence.connect(on_show_received); - stream_interactor.get_module(AvatarManager.IDENTITY).received_avatar.connect(on_received_avatar); - stream_interactor.connection_manager.connection_state_changed.connect(on_connection_changed); - stream_interactor.get_module(RosterManager.IDENTITY).updated_roster_item.connect(on_roster_updated); - muc_manager.private_room_occupant_updated.connect(on_private_room_occupant_updated); - muc_manager.room_info_updated.connect(on_room_info_updated); - } - this.cached_surface = null; - this.conversation = conversation; - this.jids = jids; - - force_redraw(); - } - - public void set_text(string text, bool gray = true) { - disconnect_stream_interactor(); - this.drawer = new AvatarDrawer().tile(null, text, null); - if (gray) drawer.grayscale(); - force_redraw(); - } -} - -} diff --git a/main/src/ui/call_window/participant_widget.vala b/main/src/ui/call_window/participant_widget.vala index 180923f1..8ec1f5ea 100644 --- a/main/src/ui/call_window/participant_widget.vala +++ b/main/src/ui/call_window/participant_widget.vala @@ -96,11 +96,11 @@ namespace Dino.Ui { shows_video = false; Box box = new Box(Orientation.HORIZONTAL, 0); box.add_css_class("video-placeholder-box"); - AvatarImage avatar = new AvatarImage() { allow_gray=false, hexpand=true, vexpand=true, halign=Align.CENTER, valign=Align.CENTER, height=100, width=100 }; + AvatarPicture avatar = new AvatarPicture() { hexpand=true, vexpand=true, halign=Align.CENTER, valign=Align.CENTER, height_request=100, width_request=100 }; if (conversation != null) { - avatar.set_conversation(stream_interactor, conversation); + avatar.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).set_conversation(conversation); } else { - avatar.set_text("?", false); + avatar.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add("?"); } box.append(avatar); diff --git a/main/src/ui/chat_input/chat_input_controller.vala b/main/src/ui/chat_input/chat_input_controller.vala index 92a12bc9..d9608a85 100644 --- a/main/src/ui/chat_input/chat_input_controller.vala +++ b/main/src/ui/chat_input/chat_input_controller.vala @@ -135,6 +135,12 @@ public class ChatInputController : Object { } string text = chat_input.chat_text_view.text_view.buffer.text; + ContentItem? quoted_content_item_bak = quoted_content_item; + + // Reset input state. Has do be done before parsing commands, because those directly return. + chat_input.chat_text_view.text_view.buffer.text = ""; + chat_input.unset_quoted_message(); + quoted_content_item = null; if (text.has_prefix("/")) { string[] token = text.split(" ", 2); @@ -189,15 +195,10 @@ public class ChatInputController : Object { } } Message out_message = stream_interactor.get_module(MessageProcessor.IDENTITY).create_out_message(text, conversation); - if (quoted_content_item != null) { - stream_interactor.get_module(Replies.IDENTITY).set_message_is_reply_to(out_message, quoted_content_item); + if (quoted_content_item_bak != null) { + stream_interactor.get_module(Replies.IDENTITY).set_message_is_reply_to(out_message, quoted_content_item_bak); } stream_interactor.get_module(MessageProcessor.IDENTITY).send_message(out_message, conversation); - - // Reset input state - chat_input.chat_text_view.text_view.buffer.text = ""; - chat_input.unset_quoted_message(); - quoted_content_item = null; } private void on_text_input_changed() { diff --git a/main/src/ui/contact_details/dialog.vala b/main/src/ui/contact_details/dialog.vala index 134bb559..c897fe4e 100644 --- a/main/src/ui/contact_details/dialog.vala +++ b/main/src/ui/contact_details/dialog.vala @@ -10,7 +10,7 @@ namespace Dino.Ui.ContactDetails { [GtkTemplate (ui = "/im/dino/Dino/contact_details_dialog.ui")] public class Dialog : Gtk.Dialog { - [GtkChild] public unowned AvatarImage avatar; + [GtkChild] public unowned AvatarPicture avatar; [GtkChild] public unowned Util.EntryLabelHybrid name_hybrid; [GtkChild] public unowned Label name_label; [GtkChild] public unowned Label jid_label; @@ -87,7 +87,7 @@ public class Dialog : Gtk.Dialog { } jid_label.label = conversation.counterpart.to_string(); account_label.label = "via " + conversation.account.bare_jid.to_string(); - avatar.set_conversation(stream_interactor, conversation); + avatar.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).set_conversation(conversation); } private void add_entry(string category, string label, string? description, Object wo) { diff --git a/main/src/ui/conversation_content_view/call_widget.vala b/main/src/ui/conversation_content_view/call_widget.vala index 4f7e2953..ab047196 100644 --- a/main/src/ui/conversation_content_view/call_widget.vala +++ b/main/src/ui/conversation_content_view/call_widget.vala @@ -94,15 +94,15 @@ namespace Dino.Ui { } foreach (Jid counterpart in call.counterparts) { - AvatarImage image = new AvatarImage() { force_gray=true, margin_top=2 }; - image.set_conversation_participant(stream_interactor, conversation, counterpart.bare_jid); - multiparty_peer_box.append(image); - multiparty_peer_widgets.add(image); + AvatarPicture picture = new AvatarPicture() { margin_top=2 }; + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add_participant(conversation, counterpart.bare_jid); + multiparty_peer_box.append(picture); + multiparty_peer_widgets.add(picture); } - AvatarImage image2 = new AvatarImage() { force_gray=true, margin_top=2 }; - image2.set_conversation_participant(stream_interactor, conversation, call.account.bare_jid); - multiparty_peer_box.append(image2); - multiparty_peer_widgets.add(image2); + AvatarPicture picture2 = new AvatarPicture() { margin_top=2 }; + picture2.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add_participant(conversation, call.account.bare_jid); + multiparty_peer_box.append(picture2); + multiparty_peer_widgets.add(picture2); outer_additional_box.add_css_class("multiparty-participants"); diff --git a/main/src/ui/conversation_content_view/chat_state_populator.vala b/main/src/ui/conversation_content_view/chat_state_populator.vala index 803739c8..2f02c635 100644 --- a/main/src/ui/conversation_content_view/chat_state_populator.vala +++ b/main/src/ui/conversation_content_view/chat_state_populator.vala @@ -68,7 +68,7 @@ private class MetaChatStateItem : Plugins.MetaConversationItem { private Conversation conversation; private Gee.List jids = new ArrayList(); private Label label; - private AvatarImage image; + private AvatarPicture picture; public MetaChatStateItem(StreamInteractor stream_interactor, Conversation conversation, Gee.List jids) { this.stream_interactor = stream_interactor; @@ -79,10 +79,10 @@ private class MetaChatStateItem : Plugins.MetaConversationItem { public override Object? get_widget(Plugins.ConversationItemWidgetInterface outer, Plugins.WidgetType widget_type) { label = new Label("") { xalign=0, vexpand=true }; label.add_css_class("dim-label"); - image = new AvatarImage() { margin_top=2, valign=Align.START }; + picture = new AvatarPicture() { margin_top=2, valign=Align.START }; Box image_content_box = new Box(Orientation.HORIZONTAL, 8); - image_content_box.append(image); + image_content_box.append(picture); image_content_box.append(label); update(); @@ -97,9 +97,7 @@ private class MetaChatStateItem : Plugins.MetaConversationItem { } private void update() { - if (image == null || label == null) return; - - image.set_conversation_participants(stream_interactor, conversation, jids.to_array()); + if (picture == null || label == null) return; Gee.List display_names = new ArrayList(); foreach (Jid jid in jids) { @@ -108,12 +106,26 @@ private class MetaChatStateItem : Plugins.MetaConversationItem { string new_text = ""; if (jids.size > 3) { new_text = _("%s, %s and %i others are typing…").printf(display_names[0], display_names[1], jids.size - 2); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor) + .add_participant(conversation, jids[0]) + .add_participant(conversation, jids[1]) + .add_participant(conversation, jids[2]) + .add("+"); } else if (jids.size == 3) { new_text = _("%s, %s and %s are typing…").printf(display_names[0], display_names[1], display_names[2]); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor) + .add_participant(conversation, jids[0]) + .add_participant(conversation, jids[1]) + .add_participant(conversation, jids[2]); } else if (jids.size == 2) { new_text =_("%s and %s are typing…").printf(display_names[0], display_names[1]); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor) + .add_participant(conversation, jids[0]) + .add_participant(conversation, jids[1]); } else { new_text = _("%s is typing…").printf(display_names[0]); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor) + .add_participant(conversation, jids[0]); } label.label = new_text; diff --git a/main/src/ui/conversation_content_view/conversation_item_skeleton.vala b/main/src/ui/conversation_content_view/conversation_item_skeleton.vala index 9e98cacb..5d86f6c7 100644 --- a/main/src/ui/conversation_content_view/conversation_item_skeleton.vala +++ b/main/src/ui/conversation_content_view/conversation_item_skeleton.vala @@ -12,7 +12,7 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface, public Grid main_grid { get; set; } public Label name_label { get; set; } public Label time_label { get; set; } - public AvatarImage avatar_image { get; set; } + public AvatarPicture avatar_picture { get; set; } public Image encryption_image { get; set; } public Image received_image { get; set; } @@ -51,7 +51,7 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface, main_grid.add_css_class("message-box"); name_label = (Label) builder.get_object("name_label"); time_label = (Label) builder.get_object("time_label"); - avatar_image = (AvatarImage) builder.get_object("avatar_image"); + avatar_picture = (AvatarPicture) builder.get_object("avatar_picture"); encryption_image = (Image) builder.get_object("encrypted_image"); received_image = (Image) builder.get_object("marked_image"); @@ -62,7 +62,8 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface, } if (item.requires_header) { - avatar_image.set_conversation_participant(stream_interactor, conversation, item.jid); + // TODO: For MUC messags, use real jid from message if known + avatar_picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add_participant(conversation, item.jid); } this.notify["show-skeleton"].connect(update_margin); @@ -71,9 +72,7 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface, ContentMetaItem? content_meta_item = item as ContentMetaItem; if (content_meta_item != null) { reactions_controller = new ReactionsController(conversation, content_meta_item.content_item, stream_interactor); - reactions_controller.box_activated.connect((widget) => { - set_widget(widget, Plugins.WidgetType.GTK4, 3); - }); + reactions_controller.box_activated.connect(on_reaction_box_activated); reactions_controller.init(); } @@ -118,7 +117,7 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface, } private void update_margin() { - avatar_image.visible = show_skeleton; + avatar_picture.visible = show_skeleton; name_label.visible = show_skeleton; time_label.visible = show_skeleton; encryption_image.visible = show_skeleton; @@ -152,7 +151,7 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface, if (item.encryption != Encryption.NONE && item.encryption != Encryption.UNKNOWN && ci != null) { string? icon_name = null; var encryption_entry = app.plugin_registry.encryption_list_entries[item.encryption]; - icon_name = encryption_entry.get_encryption_icon_name(conversation, ci.content_item); + if (encryption_entry != null) icon_name = encryption_entry.get_encryption_icon_name(conversation, ci.content_item); encryption_image.icon_name = icon_name ?? "changes-prevent-symbolic"; encryption_image.visible = true; } @@ -170,6 +169,10 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface, } } + private void on_reaction_box_activated(Widget widget) { + set_widget(widget, Plugins.WidgetType.GTK4, 3); + } + private void update_time() { time_label.label = get_relative_time(item.time.to_local()).to_string(); @@ -271,6 +274,34 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface, stream_interactor.get_module(RosterManager.IDENTITY).disconnect(updated_roster_handler_id); updated_roster_handler_id = 0; } + reactions_controller = null; + + // Children won't be disposed automatically + if (name_label != null) { + name_label.unparent(); + name_label.dispose(); + name_label = null; + } + if (time_label != null) { + time_label.unparent(); + time_label.dispose(); + time_label = null; + } + if (avatar_picture != null) { + avatar_picture.unparent(); + avatar_picture.dispose(); + avatar_picture = null; + } + if (encryption_image != null) { + encryption_image.unparent(); + encryption_image.dispose(); + encryption_image = null; + } + if (received_image != null) { + received_image.unparent(); + received_image.dispose(); + received_image = null; + } base.dispose(); } } diff --git a/main/src/ui/conversation_content_view/conversation_view.vala b/main/src/ui/conversation_content_view/conversation_view.vala index 36e19474..badc6c65 100644 --- a/main/src/ui/conversation_content_view/conversation_view.vala +++ b/main/src/ui/conversation_content_view/conversation_view.vala @@ -20,7 +20,7 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug [GtkChild] private unowned Box main; [GtkChild] private unowned Box main_wrap_box; - private ArrayList action_buttons = new ArrayList(); + private HashMap action_buttons = new HashMap(); private Gee.List? message_actions = null; private StreamInteractor stream_interactor; @@ -46,6 +46,30 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug construct { this.layout_manager = new BinLayout(); + + // Setup all message menu buttons + var correction_button = new Button() { name="correction" }; + correction_button.clicked.connect((button) => { + on_action_button_clicked(button, null); + }); + action_buttons["correction"] = correction_button; + message_menu_box.append(correction_button); + + var reply_button = new Button() { name="reply" }; + reply_button.clicked.connect((button) => { + on_action_button_clicked(button, null); + }); + action_buttons["reply"] = reply_button; + message_menu_box.append(reply_button); + + var reaction_button = new MenuButton() { name="reaction" }; + EmojiChooser chooser = new EmojiChooser(); + chooser.emoji_picked.connect((emoji) => { + on_action_button_clicked(reaction_button, new GLib.Variant.string(emoji)); + }); + reaction_button.popover = chooser; + action_buttons["reaction"] = reaction_button; + message_menu_box.append(reaction_button); } public ConversationView init(StreamInteractor stream_interactor) { @@ -112,7 +136,7 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug } private bool is_highlight_fixed() { - foreach (Widget widget in action_buttons) { + foreach (Widget widget in action_buttons.values) { MenuButton? menu_button = widget as MenuButton; if (menu_button != null && menu_button.popover.visible) return true; @@ -192,39 +216,32 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug return; } - foreach (Widget widget in action_buttons) { - message_menu_box.remove(widget); - } - action_buttons.clear(); + var current_message_actions = current_meta_item.get_item_actions(Plugins.WidgetType.GTK4); message_actions = current_meta_item.get_item_actions(Plugins.WidgetType.GTK4); if (message_actions != null) { message_menu_box.visible = true; - // Configure as many buttons as we need with the actions for the current meta item - foreach (var message_action in message_actions) { - if (message_action.popover != null) { - MenuButton button = new MenuButton(); - button.sensitive = message_action.sensitive; - button.icon_name = message_action.icon_name; - button.set_popover(message_action.popover as Popover); - button.tooltip_text = Util.string_if_tooltips_active(message_action.tooltip); - action_buttons.add(button); - } else if (message_action.callback != null) { - Button button = new Button(); - button.sensitive = message_action.sensitive; - button.icon_name = message_action.icon_name; - button.clicked.connect(() => { - message_action.callback(button, current_meta_item, currently_highlighted); - }); - button.tooltip_text = Util.string_if_tooltips_active(message_action.tooltip); - action_buttons.add(button); - } + foreach (Widget widget in action_buttons.values) { + widget.visible = false; } - foreach (Widget widget in action_buttons) { - message_menu_box.append(widget); + // Configure as many buttons as we need with the actions for the current meta item + foreach (var message_action in current_message_actions) { + Widget button_widget = action_buttons[message_action.name]; + button_widget.visible = true; + if (message_action.name == "reaction") { + MenuButton button = (MenuButton) button_widget; + button.sensitive = message_action.sensitive; + button.icon_name = message_action.icon_name; + button.tooltip_text = Util.string_if_tooltips_active(message_action.tooltip); + } else if (message_action.callback != null) { + Button button = (Button) button_widget; + button.sensitive = message_action.sensitive; + button.icon_name = message_action.icon_name; + button.tooltip_text = Util.string_if_tooltips_active(message_action.tooltip); + } } } else { message_menu_box.visible = false; @@ -409,6 +426,7 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug content_items.remove((ContentMetaItem)item); } meta_items.remove(item); + skeleton.dispose(); } removed_item(item); @@ -498,12 +516,10 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug (upper_item.mark == Message.Marked.WONTSEND) == (lower_item.mark == Message.Marked.WONTSEND); } - private void on_action_button_clicked(ToggleButton button) { - int button_idx = action_buttons.index_of(button); - print(button_idx.to_string() + "\n"); - Plugins.MessageAction message_action = message_actions[button_idx]; - if (message_action.callback != null) { - message_action.callback(button, current_meta_item, currently_highlighted); + private void on_action_button_clicked(Widget widget, GLib.Variant? variant = null) { + foreach (var action in message_actions) { + if (action.name != widget.name) continue; + action.callback(variant); } } @@ -576,12 +592,19 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug private void clear() { was_upper = null; was_page_size = null; + foreach (var item in content_items) { + item.dispose(); + } content_items.clear(); meta_items.clear(); widget_order.clear(); + foreach (var skeleton in item_item_skeletons.values) { + skeleton.dispose(); + } item_item_skeletons.clear(); foreach (Widget widget in widgets.values) { - main.remove(widget); + widget.unparent(); + widget.dispose(); } widgets.clear(); } diff --git a/main/src/ui/conversation_content_view/file_widget.vala b/main/src/ui/conversation_content_view/file_widget.vala index 9c2d5c59..f522356b 100644 --- a/main/src/ui/conversation_content_view/file_widget.vala +++ b/main/src/ui/conversation_content_view/file_widget.vala @@ -108,7 +108,7 @@ public class FileWidget : SizeRequestBox { } catch (Error e) { } } - if (state != State.DEFAULT) { + if (!show_image() && state != State.DEFAULT) { if (content != null) this.remove(content); FileDefaultWidget default_file_widget = new FileDefaultWidget(); default_widget_controller = new FileDefaultWidgetController(default_file_widget); @@ -121,8 +121,12 @@ public class FileWidget : SizeRequestBox { private bool show_image() { if (file_transfer.mime_type == null) return false; - if (file_transfer.state != FileTransfer.State.COMPLETE && - !(file_transfer.direction == FileTransfer.DIRECTION_SENT && file_transfer.state == FileTransfer.State.IN_PROGRESS)) { + + // If the image is being sent by this client, we already have the file + bool in_progress_from_us = file_transfer.direction == FileTransfer.DIRECTION_SENT && + file_transfer.ourpart.equals(file_transfer.account.full_jid) && + file_transfer.state == FileTransfer.State.IN_PROGRESS; + if (file_transfer.state != FileTransfer.State.COMPLETE && !in_progress_from_us) { return false; } @@ -135,6 +139,17 @@ public class FileWidget : SizeRequestBox { } return false; } + + public override void dispose() { + if (default_widget_controller != null) default_widget_controller.dispose(); + default_widget_controller = null; + if (content != null) { + content.unparent(); + content.dispose(); + content = null; + } + base.dispose(); + } } public class FileWidgetController : Object { diff --git a/main/src/ui/conversation_content_view/item_actions.vala b/main/src/ui/conversation_content_view/item_actions.vala index 2cca7565..d4195bdd 100644 --- a/main/src/ui/conversation_content_view/item_actions.vala +++ b/main/src/ui/conversation_content_view/item_actions.vala @@ -4,14 +4,14 @@ using Gtk; namespace Dino.Ui { public Plugins.MessageAction get_reaction_action(ContentItem content_item, Conversation conversation, StreamInteractor stream_interactor) { Plugins.MessageAction action = new Plugins.MessageAction(); + action.name = "reaction"; action.icon_name = "dino-emoticon-add-symbolic"; action.tooltip = _("Add reaction"); - EmojiChooser chooser = new EmojiChooser(); - chooser.emoji_picked.connect((emoji) => { + action.callback = (variant) => { + string emoji = variant.get_string(); stream_interactor.get_module(Reactions.IDENTITY).add_reaction(conversation, content_item, emoji); - }); - action.popover = chooser; + }; // Disable the button if reaction aren't possible. bool supports_reactions = stream_interactor.get_module(Reactions.IDENTITY).conversation_supports_reactions(conversation); @@ -29,9 +29,10 @@ namespace Dino.Ui { public Plugins.MessageAction get_reply_action(ContentItem content_item, Conversation conversation, StreamInteractor stream_interactor) { Plugins.MessageAction action = new Plugins.MessageAction(); + action.name = "reply"; action.icon_name = "mail-reply-sender-symbolic"; action.tooltip = _("Reply"); - action.callback = (button, content_meta_item_activated, widget) => { + action.callback = () => { GLib.Application.get_default().activate_action("quote", new GLib.Variant.tuple(new GLib.Variant[] { new GLib.Variant.int32(conversation.id), new GLib.Variant.int32(content_item.id) })); }; diff --git a/main/src/ui/conversation_content_view/message_widget.vala b/main/src/ui/conversation_content_view/message_widget.vala index 0a58db7b..5a18b51c 100644 --- a/main/src/ui/conversation_content_view/message_widget.vala +++ b/main/src/ui/conversation_content_view/message_widget.vala @@ -19,6 +19,7 @@ public class MessageMetaItem : ContentMetaItem { private StreamInteractor stream_interactor; private MessageItem message_item; public Message.Marked marked { get; set; } + public Plugins.ConversationItemWidgetInterface outer = null; MessageItemEditMode? edit_mode = null; ChatTextViewController? controller = null; @@ -35,6 +36,8 @@ public class MessageMetaItem : ContentMetaItem { message_item = content_item as MessageItem; this.stream_interactor = stream_interactor; + stream_interactor.get_module(MessageCorrection.IDENTITY).received_correction.connect(on_received_correction); + label.activate_link.connect(on_label_activate_link); Message message = ((MessageItem) content_item).message; @@ -146,43 +149,9 @@ public class MessageMetaItem : ContentMetaItem { } public override Object? get_widget(Plugins.ConversationItemWidgetInterface outer, Plugins.WidgetType type) { + this.outer = outer; - stream_interactor.get_module(MessageCorrection.IDENTITY).received_correction.connect(on_received_correction); - - this.notify["in-edit-mode"].connect(() => { - if (in_edit_mode == false) return; - bool allowed = stream_interactor.get_module(MessageCorrection.IDENTITY).is_own_correction_allowed(message_item.conversation, message_item.message); - if (allowed) { - MessageItem message_item = content_item as MessageItem; - Message message = message_item.message; - - edit_mode = new MessageItemEditMode(); - controller = new ChatTextViewController(edit_mode.chat_text_view, stream_interactor); - Conversation conversation = message_item.conversation; - controller.initialize_for_conversation(conversation); - - edit_mode.cancelled.connect(() => { - in_edit_mode = false; - outer.set_widget(label, Plugins.WidgetType.GTK4, 2); - }); - edit_mode.send.connect(() => { - if (((MessageItem) content_item).message.body != edit_mode.chat_text_view.text_view.buffer.text) { - on_edit_send(edit_mode.chat_text_view.text_view.buffer.text); - } else { -// edit_cancelled(); - } - in_edit_mode = false; - outer.set_widget(label, Plugins.WidgetType.GTK4, 2); - }); - - edit_mode.chat_text_view.text_view.buffer.text = message.body; - - outer.set_widget(edit_mode, Plugins.WidgetType.GTK4, 2); - edit_mode.chat_text_view.text_view.grab_focus(); - } else { - this.in_edit_mode = false; - } - }); + this.notify["in-edit-mode"].connect(on_in_edit_mode_changed); outer.set_widget(label, Plugins.WidgetType.GTK4, 2); @@ -201,7 +170,6 @@ public class MessageMetaItem : ContentMetaItem { } public override Gee.List? get_item_actions(Plugins.WidgetType type) { - if (content_item as FileItem != null || this.in_edit_mode) return null; if (in_edit_mode) return null; Gee.List actions = new ArrayList(); @@ -209,9 +177,10 @@ public class MessageMetaItem : ContentMetaItem { bool correction_allowed = stream_interactor.get_module(MessageCorrection.IDENTITY).is_own_correction_allowed(message_item.conversation, message_item.message); if (correction_allowed) { Plugins.MessageAction action1 = new Plugins.MessageAction(); + action1.name = "correction"; action1.icon_name = "document-edit-symbolic"; action1.tooltip = _("Edit message"); - action1.callback = (button, content_meta_item_activated, widget) => { + action1.callback = () => { this.in_edit_mode = true; }; actions.add(action1); @@ -223,6 +192,41 @@ public class MessageMetaItem : ContentMetaItem { return actions; } + private void on_in_edit_mode_changed() { + if (in_edit_mode == false) return; + bool allowed = stream_interactor.get_module(MessageCorrection.IDENTITY).is_own_correction_allowed(message_item.conversation, message_item.message); + if (allowed) { + MessageItem message_item = content_item as MessageItem; + Message message = message_item.message; + + edit_mode = new MessageItemEditMode(); + controller = new ChatTextViewController(edit_mode.chat_text_view, stream_interactor); + Conversation conversation = message_item.conversation; + controller.initialize_for_conversation(conversation); + + edit_mode.cancelled.connect(() => { + in_edit_mode = false; + outer.set_widget(label, Plugins.WidgetType.GTK4, 2); + }); + edit_mode.send.connect(() => { + if (((MessageItem) content_item).message.body != edit_mode.chat_text_view.text_view.buffer.text) { + on_edit_send(edit_mode.chat_text_view.text_view.buffer.text); + } else { +// edit_cancelled(); + } + in_edit_mode = false; + outer.set_widget(label, Plugins.WidgetType.GTK4, 2); + }); + + edit_mode.chat_text_view.text_view.buffer.text = message.body; + + outer.set_widget(edit_mode, Plugins.WidgetType.GTK4, 2); + edit_mode.chat_text_view.text_view.grab_focus(); + } else { + this.in_edit_mode = false; + } + } + private void on_edit_send(string text) { stream_interactor.get_module(MessageCorrection.IDENTITY).send_correction(message_item.conversation, message_item.message, text); this.in_edit_mode = false; @@ -251,6 +255,17 @@ public class MessageMetaItem : ContentMetaItem { Dino.Application.get_default().open(new File[]{file}, ""); return true; } + + public override void dispose() { + stream_interactor.get_module(MessageCorrection.IDENTITY).received_correction.disconnect(on_received_correction); + this.notify["in-edit-mode"].disconnect(on_in_edit_mode_changed); + if (label != null) { + label.unparent(); + label.dispose(); + label = null; + } + base.dispose(); + } } [GtkTemplate (ui = "/im/dino/Dino/message_item_widget_edit_mode.ui")] diff --git a/main/src/ui/conversation_content_view/quote_widget.vala b/main/src/ui/conversation_content_view/quote_widget.vala index 6dbf459c..23b62e6a 100644 --- a/main/src/ui/conversation_content_view/quote_widget.vala +++ b/main/src/ui/conversation_content_view/quote_widget.vala @@ -61,13 +61,13 @@ namespace Dino.Ui.Quote { public Widget get_widget(Model model) { Builder builder = new Builder.from_resource("/im/dino/Dino/quote.ui"); - AvatarImage avatar = (AvatarImage) builder.get_object("avatar"); + AvatarPicture avatar = (AvatarPicture) builder.get_object("avatar"); Label author = (Label) builder.get_object("author"); Label time = (Label) builder.get_object("time"); Label message = (Label) builder.get_object("message"); Button abort_button = (Button) builder.get_object("abort-button"); - avatar.set_conversation_participant(model.stream_interactor, model.conversation, model.author_jid); + avatar.model = new ViewModel.CompatAvatarPictureModel(model.stream_interactor).add_participant(model.conversation, model.author_jid); model.bind_property("display-name", author, "label", BindingFlags.SYNC_CREATE); model.bind_property("display-time", time, "label", BindingFlags.SYNC_CREATE); model.bind_property("message", message, "label", BindingFlags.SYNC_CREATE); diff --git a/main/src/ui/conversation_selector/conversation_selector_row.vala b/main/src/ui/conversation_selector/conversation_selector_row.vala index e71176aa..1bcf6962 100644 --- a/main/src/ui/conversation_selector/conversation_selector_row.vala +++ b/main/src/ui/conversation_selector/conversation_selector_row.vala @@ -12,7 +12,7 @@ namespace Dino.Ui { [GtkTemplate (ui = "/im/dino/Dino/conversation_row.ui")] public class ConversationSelectorRow : ListBoxRow { - [GtkChild] protected unowned AvatarImage image; + [GtkChild] protected unowned AvatarPicture picture; [GtkChild] protected unowned Label name_label; [GtkChild] protected unowned Label time_label; [GtkChild] protected unowned Label nick_label; @@ -101,7 +101,7 @@ public class ConversationSelectorRow : ListBoxRow { x_button.clicked.connect(() => { stream_interactor.get_module(ConversationManager.IDENTITY).close_conversation(conversation); }); - image.set_conversation(stream_interactor, conversation); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).set_conversation(conversation); conversation.notify["read-up-to-item"].connect(() => update_read()); conversation.notify["pinned"].connect(() => { update_pinned_icon(); }); @@ -225,7 +225,21 @@ public class ConversationSelectorRow : ListBoxRow { label.attributes = copy; } + private bool update_read_pending = false; + private bool update_read_pending_force = false; protected void update_read(bool force_update = false) { + if (force_update) update_read_pending_force = true; + if (update_read_pending) return; + update_read_pending = true; + Idle.add(() => { + update_read_pending = false; + update_read_pending_force = false; + update_read_idle(update_read_pending_force); + return Source.REMOVE; + }, Priority.LOW); + } + + private void update_read_idle(bool force_update = false) { int current_num_unread = stream_interactor.get_module(ChatInteraction.IDENTITY).get_num_unread(conversation); if (num_unread == current_num_unread && !force_update) return; num_unread = current_num_unread; diff --git a/main/src/ui/global_search.vala b/main/src/ui/global_search.vala index d206220d..6872f631 100644 --- a/main/src/ui/global_search.vala +++ b/main/src/ui/global_search.vala @@ -116,15 +116,15 @@ public class GlobalSearch { // Populate new suggestions foreach(SearchSuggestion suggestion in suggestions) { Builder builder = new Builder.from_resource("/im/dino/Dino/search_autocomplete.ui"); - AvatarImage avatar = (AvatarImage)builder.get_object("image"); + AvatarPicture avatar = (AvatarPicture)builder.get_object("picture"); Label label = (Label)builder.get_object("label"); string display_name; if (suggestion.conversation.type_ == Conversation.Type.GROUPCHAT && !suggestion.conversation.counterpart.equals(suggestion.jid) || suggestion.conversation.type_ == Conversation.Type.GROUPCHAT_PM) { display_name = Util.get_participant_display_name(stream_interactor, suggestion.conversation, suggestion.jid); - avatar.set_conversation_participant(stream_interactor, suggestion.conversation, suggestion.jid); + avatar.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add_participant(suggestion.conversation, suggestion.jid); } else { display_name = Util.get_conversation_display_name(stream_interactor, suggestion.conversation); - avatar.set_conversation(stream_interactor, suggestion.conversation); + avatar.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).set_conversation(suggestion.conversation); } if (display_name != suggestion.jid.to_string()) { label.set_markup("%s %s".printf(Markup.escape_text(display_name), Markup.escape_text(suggestion.jid.to_string()))); @@ -289,10 +289,10 @@ public class GlobalSearch { } private Grid get_skeleton(MessageItem item) { - AvatarImage image = new AvatarImage() { height=32, width=32, margin_end=7, valign=Align.START, allow_gray = false }; - image.set_conversation_participant(stream_interactor, item.conversation, item.jid); + AvatarPicture picture = new AvatarPicture() { height_request=32, width_request=32, margin_end=7, valign=Align.START }; + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add_participant(item.conversation, item.jid); Grid grid = new Grid() { row_homogeneous=false }; - grid.attach(image, 0, 0, 1, 2); + grid.attach(picture, 0, 0, 1, 2); string display_name = Util.get_participant_display_name(stream_interactor, item.conversation, item.jid); Label name_label = new Label(display_name) { ellipsize=EllipsizeMode.END, xalign=0 }; diff --git a/main/src/ui/manage_accounts/account_row.vala b/main/src/ui/manage_accounts/account_row.vala index b3a33eae..ae734b83 100644 --- a/main/src/ui/manage_accounts/account_row.vala +++ b/main/src/ui/manage_accounts/account_row.vala @@ -7,7 +7,7 @@ namespace Dino.Ui.ManageAccounts { [GtkTemplate (ui = "/im/dino/Dino/manage_accounts/account_row.ui")] public class AccountRow : Gtk.ListBoxRow { - [GtkChild] public unowned AvatarImage image; + [GtkChild] public unowned AvatarPicture picture; [GtkChild] public unowned Label jid_label; [GtkChild] public unowned Image icon; @@ -17,7 +17,7 @@ public class AccountRow : Gtk.ListBoxRow { public AccountRow(StreamInteractor stream_interactor, Account account) { this.stream_interactor = stream_interactor; this.account = account; - image.set_conversation(stream_interactor, new Conversation(account.bare_jid, account, Conversation.Type.CHAT)); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add_participant(new Conversation(account.bare_jid, account, Conversation.Type.CHAT), account.bare_jid); jid_label.set_label(account.bare_jid.to_string()); stream_interactor.connection_manager.connection_error.connect((account, error) => { diff --git a/main/src/ui/manage_accounts/dialog.vala b/main/src/ui/manage_accounts/dialog.vala index 0a37b052..a326aeff 100644 --- a/main/src/ui/manage_accounts/dialog.vala +++ b/main/src/ui/manage_accounts/dialog.vala @@ -19,7 +19,7 @@ public class Dialog : Gtk.Dialog { [GtkChild] public unowned Button no_accounts_add; [GtkChild] public unowned Button add_account_button; [GtkChild] public unowned Button remove_account_button; - [GtkChild] public unowned AvatarImage image; + [GtkChild] public unowned AvatarPicture picture; [GtkChild] public unowned Button image_button; [GtkChild] public unowned Label jid_label; [GtkChild] public unowned Label state_label; @@ -178,14 +178,14 @@ public class Dialog : Gtk.Dialog { private void on_received_avatar(Jid jid, Account account) { if (selected_account.equals(account) && jid.equals(account.bare_jid)) { - image.set_conversation(stream_interactor, new Conversation(account.bare_jid, account, Conversation.Type.CHAT)); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add_participant(new Conversation(account.bare_jid, account, Conversation.Type.CHAT), account.bare_jid); } } private void populate_grid_data(Account account) { active_switch.state_set.disconnect(change_account_state); - image.set_conversation(stream_interactor, new Conversation(account.bare_jid, account, Conversation.Type.CHAT)); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add_participant(new Conversation(account.bare_jid, account, Conversation.Type.CHAT), account.bare_jid); active_switch.set_active(account.enabled); jid_label.label = account.bare_jid.to_string(); diff --git a/main/src/ui/notifier_freedesktop.vala b/main/src/ui/notifier_freedesktop.vala index b6b31d34..3f390102 100644 --- a/main/src/ui/notifier_freedesktop.vala +++ b/main/src/ui/notifier_freedesktop.vala @@ -94,6 +94,7 @@ public class Dino.Ui.FreeDesktopNotifier : NotificationProvider, Object { HashTable hash_table = new HashTable(null, null); hash_table["image-data"] = yield get_conversation_icon(conversation); hash_table["desktop-entry"] = new Variant.string(Dino.Application.get_default().get_application_id()); + hash_table["category"] = new Variant.string("im.received"); string[] actions = new string[] {"default", "Open conversation"}; try { uint32 notification_id = yield dbus_notifications.notify("Dino", replace_id, "", conversation_display_name, body, actions, hash_table, -1); @@ -120,6 +121,7 @@ public class Dino.Ui.FreeDesktopNotifier : NotificationProvider, Object { hash_table["sound-name"] = new Variant.string("phone-incoming-call"); hash_table["urgency"] = new Variant.byte(2); hash_table["desktop-entry"] = new Variant.string(Dino.Application.get_default().get_application_id()); + hash_table["category"] = new Variant.string("im"); string[] actions = new string[] {"default", "Open conversation", "reject", _("Reject"), "accept", _("Accept")}; try { uint32 notification_id = yield dbus_notifications.notify("Dino", 0, "", summary, body, actions, hash_table, 0); @@ -158,6 +160,7 @@ public class Dino.Ui.FreeDesktopNotifier : NotificationProvider, Object { HashTable hash_table = new HashTable(null, null); hash_table["image-data"] = yield get_conversation_icon(conversation); hash_table["desktop-entry"] = new Variant.string(Dino.Application.get_default().get_application_id()); + hash_table["category"] = new Variant.string("im"); string[] actions = new string[] {"default", "Open conversation", "accept", _("Accept"), "deny", _("Deny")}; try { uint32 notification_id = yield dbus_notifications.notify("Dino", 0, "", summary, body, actions, hash_table, -1); @@ -197,6 +200,7 @@ public class Dino.Ui.FreeDesktopNotifier : NotificationProvider, Object { HashTable hash_table = new HashTable(null, null); hash_table["desktop-entry"] = new Variant.string(Dino.Application.get_default().get_application_id()); + hash_table["category"] = new Variant.string("im.error"); try { yield dbus_notifications.notify("Dino", 0, "im.dino.Dino", summary, body, new string[]{}, hash_table, -1); } catch (Error e) { @@ -217,6 +221,7 @@ public class Dino.Ui.FreeDesktopNotifier : NotificationProvider, Object { HashTable hash_table = new HashTable(null, null); hash_table["image-data"] = yield get_conversation_icon(direct_conversation); hash_table["desktop-entry"] = new Variant.string(Dino.Application.get_default().get_application_id()); + hash_table["category"] = new Variant.string("im"); string[] actions = new string[] {"default", "", "reject", _("Reject"), "accept", _("Accept")}; try { @@ -250,6 +255,7 @@ public class Dino.Ui.FreeDesktopNotifier : NotificationProvider, Object { HashTable hash_table = new HashTable(null, null); hash_table["image-data"] = yield get_conversation_icon(conversation); hash_table["desktop-entry"] = new Variant.string(Dino.Application.get_default().get_application_id()); + hash_table["category"] = new Variant.string("im"); string[] actions = new string[] {"deny", _("Deny"), "accept", _("Accept")}; try { @@ -287,8 +293,12 @@ public class Dino.Ui.FreeDesktopNotifier : NotificationProvider, Object { } private async Variant get_conversation_icon(Conversation conversation) { - AvatarDrawer drawer = yield Util.get_conversation_avatar_drawer(stream_interactor, conversation); - Cairo.ImageSurface surface = drawer.size(40, 40).draw_image_surface(); + CompatAvatarDrawer drawer = new CompatAvatarDrawer() { + model = new ViewModel.CompatAvatarPictureModel(stream_interactor).set_conversation(conversation), + width_request = 40, + height_request = 40 + }; + Cairo.ImageSurface surface = drawer.draw_image_surface(); Gdk.Pixbuf avatar = Gdk.pixbuf_get_from_surface(surface, 0, 0, surface.get_width(), surface.get_height()); var bytes = avatar.pixel_bytes; var image_bytes = Variant.new_from_data(new VariantType("ay"), bytes.get_data(), true, bytes); diff --git a/main/src/ui/notifier_gnotifications.vala b/main/src/ui/notifier_gnotifications.vala index d569a358..90c8ca8c 100644 --- a/main/src/ui/notifier_gnotifications.vala +++ b/main/src/ui/notifier_gnotifications.vala @@ -171,8 +171,12 @@ namespace Dino.Ui { } private async Icon get_conversation_icon(Conversation conversation) throws Error { - AvatarDrawer drawer = yield Util.get_conversation_avatar_drawer(stream_interactor, conversation); - Cairo.ImageSurface surface = drawer.size(40, 40).draw_image_surface(); + CompatAvatarDrawer drawer = new CompatAvatarDrawer() { + model = new ViewModel.CompatAvatarPictureModel(stream_interactor).set_conversation(conversation), + width_request = 40, + height_request = 40 + }; + Cairo.ImageSurface surface = drawer.draw_image_surface(); Gdk.Pixbuf avatar = Gdk.pixbuf_get_from_surface(surface, 0, 0, surface.get_width(), surface.get_height()); uint8[] buffer; avatar.save_to_buffer(out buffer, "png"); diff --git a/main/src/ui/occupant_menu/list_row.vala b/main/src/ui/occupant_menu/list_row.vala index 6b43fe7f..476d9e61 100644 --- a/main/src/ui/occupant_menu/list_row.vala +++ b/main/src/ui/occupant_menu/list_row.vala @@ -8,7 +8,7 @@ namespace Dino.Ui.OccupantMenu { public class ListRow : Object { private Grid main_grid; - private AvatarImage image; + private AvatarPicture picture; public Label name_label; public Conversation? conversation; @@ -17,7 +17,7 @@ public class ListRow : Object { construct { Builder builder = new Builder.from_resource("/im/dino/Dino/occupant_list_item.ui"); main_grid = (Grid) builder.get_object("main_grid"); - image = (AvatarImage) builder.get_object("image"); + picture = (AvatarPicture) builder.get_object("picture"); name_label = (Label) builder.get_object("name_label"); } @@ -26,12 +26,12 @@ public class ListRow : Object { this.jid = jid; name_label.label = Util.get_participant_display_name(stream_interactor, conversation, jid); - image.set_conversation_participant(stream_interactor, conversation, jid); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add_participant(conversation, jid); } public ListRow.label(string c, string text) { name_label.label = text; - image.set_text(c); + picture.model = new ViewModel.CompatAvatarPictureModel(null).add(c); } public Widget get_widget() { diff --git a/main/src/ui/util/helper.vala b/main/src/ui/util/helper.vala index 485e469f..d6da72dd 100644 --- a/main/src/ui/util/helper.vala +++ b/main/src/ui/util/helper.vala @@ -3,6 +3,7 @@ using Gtk; using Dino.Entities; using Xmpp; +using Xmpp.Xep; namespace Dino.Ui.Util { @@ -18,20 +19,22 @@ private const string[] material_colors_500 = {"F44336", "E91E63", "9C27B0", "673 private const string[] material_colors_300 = {"E57373", "F06292", "BA68C8", "9575CD", "7986CB", "64B5F6", "4FC3F7", "4DD0E1", "4DB6AC", "81C784", "AED581", "DCE775", "FFD54F", "FFB74D", "FF8A65", "A1887F"}; private const string[] material_colors_200 = {"EF9A9A", "F48FB1", "CE93D8", "B39DDB", "9FA8DA", "90CAF9", "81D4FA", "80DEEA", "80CBC4", "A5D6A7", "C5E1A5", "E6EE9C", "FFE082", "FFCC80", "FFAB91", "BCAAA4"}; +public static string get_consistent_hex_color(StreamInteractor stream_interactor, Account account, Jid jid, bool dark_theme = false) { + uint8[] rgb; + if (stream_interactor.get_module(MucManager.IDENTITY).is_groupchat(jid.bare_jid, account) && jid.resourcepart != null) { + rgb = ConsistentColor.string_to_rgb(jid.resourcepart); + } else { + rgb = ConsistentColor.string_to_rgb(jid.bare_jid.to_string()); + } + return "%.2x%.2x%.2x".printf(rgb[0], rgb[1], rgb[2]); +} + public static string get_avatar_hex_color(StreamInteractor stream_interactor, Account account, Jid jid, Conversation? conversation = null) { - uint hash = get_relevant_jid(stream_interactor, account, jid, conversation).to_string().hash(); - return material_colors_300[hash % material_colors_300.length]; -// return tango_colors_light[name.hash() % tango_colors_light.length]; + return get_consistent_hex_color(stream_interactor, account, get_relevant_jid(stream_interactor, account, jid, conversation)); } public static string get_name_hex_color(StreamInteractor stream_interactor, Account account, Jid jid, bool dark_theme = false, Conversation? conversation = null) { - uint hash = get_relevant_jid(stream_interactor, account, jid, conversation).to_string().hash(); - if (dark_theme) { - return material_colors_300[hash % material_colors_300.length]; - } else { - return material_colors_500[hash % material_colors_500.length]; - } -// return tango_colors_medium[name.hash() % tango_colors_medium.length]; + return get_consistent_hex_color(stream_interactor, account, get_relevant_jid(stream_interactor, account, jid, conversation), dark_theme); } private static Jid get_relevant_jid(StreamInteractor stream_interactor, Account account, Jid jid, Conversation? conversation = null) { @@ -58,62 +61,6 @@ public static string color_for_show(string show) { } } -public static async AvatarDrawer get_conversation_avatar_drawer(StreamInteractor stream_interactor, Conversation conversation) { - return yield get_conversation_participants_avatar_drawer(stream_interactor, conversation, new Jid[0]); -} - -public static async AvatarDrawer get_conversation_participants_avatar_drawer(StreamInteractor stream_interactor, Conversation conversation, owned Jid[] jids) { - AvatarManager avatar_manager = stream_interactor.get_module(AvatarManager.IDENTITY); - MucManager muc_manager = stream_interactor.get_module(MucManager.IDENTITY); - if (conversation.type_ != Conversation.Type.GROUPCHAT) { - Jid jid = jids.length == 1 ? jids[0] : conversation.counterpart; - Jid avatar_jid = jid; - if (conversation.type_ == Conversation.Type.GROUPCHAT_PM) avatar_jid = muc_manager.get_real_jid(avatar_jid, conversation.account) ?? avatar_jid; - return new AvatarDrawer().tile(yield avatar_manager.get_avatar(conversation.account, avatar_jid), jids.length == 1 ? - get_participant_display_name(stream_interactor, conversation, jid) : - get_conversation_display_name(stream_interactor, conversation), - Util.get_avatar_hex_color(stream_interactor, conversation.account, jid, conversation)); - } - if (jids.length > 0) { - AvatarDrawer drawer = new AvatarDrawer(); - for (int i = 0; i < (jids.length <= 4 ? jids.length : 3); i++) { - Jid avatar_jid = jids[i]; - Gdk.Pixbuf? part_avatar = yield avatar_manager.get_avatar(conversation.account, avatar_jid); - if (part_avatar == null && avatar_jid.equals_bare(conversation.counterpart) && muc_manager.is_private_room(conversation.account, conversation.counterpart)) { - avatar_jid = muc_manager.get_real_jid(avatar_jid, conversation.account) ?? avatar_jid; - part_avatar = yield avatar_manager.get_avatar(conversation.account, avatar_jid); - } - drawer.tile(part_avatar, get_participant_display_name(stream_interactor, conversation, jids[i]), - Util.get_avatar_hex_color(stream_interactor, conversation.account, jids[i], conversation)); - } - if (jids.length > 4) { - drawer.plus(); - } - return drawer; - } - Gdk.Pixbuf? room_avatar = yield avatar_manager.get_avatar(conversation.account, conversation.counterpart); - Gee.List? occupants = muc_manager.get_other_offline_members(conversation.counterpart, conversation.account); - if (room_avatar != null || !muc_manager.is_private_room(conversation.account, conversation.counterpart) || occupants == null || occupants.size == 0) { - return new AvatarDrawer().tile(room_avatar, "#", Util.get_avatar_hex_color(stream_interactor, conversation.account, conversation.counterpart, conversation)); - } - AvatarDrawer drawer = new AvatarDrawer(); - for (int i = 0; i < (occupants.size <= 4 ? occupants.size : 3); i++) { - Jid jid = occupants[i]; - Jid avatar_jid = jid; - Gdk.Pixbuf? part_avatar = yield avatar_manager.get_avatar(conversation.account, avatar_jid); - if (part_avatar == null && avatar_jid.equals_bare(conversation.counterpart) && muc_manager.is_private_room(conversation.account, conversation.counterpart)) { - avatar_jid = muc_manager.get_real_jid(avatar_jid, conversation.account) ?? avatar_jid; - part_avatar = yield avatar_manager.get_avatar(conversation.account, avatar_jid); - } - drawer.tile(part_avatar, get_participant_display_name(stream_interactor, conversation, jid), - Util.get_avatar_hex_color(stream_interactor, conversation.account, jid, conversation)); - } - if (occupants.size > 4) { - drawer.plus(); - } - return drawer; -} - public static string get_conversation_display_name(StreamInteractor stream_interactor, Conversation conversation) { return Dino.get_conversation_display_name(stream_interactor, conversation, _("%s from %s")); } @@ -134,27 +81,6 @@ public static string get_occupant_display_name(StreamInteractor stream_interacto return Dino.get_occupant_display_name(stream_interactor, conversation, jid, me_is_me ? _("Me") : null); } -// TODO this has no usages? -//public static void image_set_from_scaled_pixbuf(Image image, Gdk.Pixbuf pixbuf, int scale = 0, int width = 0, int height = 0) { -// if (scale == 0) scale = image.scale_factor; -// Cairo.Surface surface = Gdk.cairo_surface_create_from_pixbuf(pixbuf, scale, image.get_window()); -// if (height == 0 && width != 0) { -// height = (int) ((double) width / pixbuf.width * pixbuf.height); -// } else if (height != 0 && width == 0) { -// width = (int) ((double) height / pixbuf.height * pixbuf.width); -// } -// if (width != 0) { -// Cairo.Surface surface_new = new Cairo.Surface.similar_image(surface, Cairo.Format.ARGB32, width, height); -// Cairo.Context context = new Cairo.Context(surface_new); -// context.scale((double) width * scale / pixbuf.width, (double) height * scale / pixbuf.height); -// context.set_source_surface(surface, 0, 0); -// context.get_source().set_filter(Cairo.Filter.BEST); -// context.paint(); -// surface = surface_new; -// } -// image.set_from_surface(surface); -//} - public static Gdk.RGBA get_label_pango_color(Label label, string css_color) { Gtk.CssProvider provider = force_color(label, css_color); Gdk.RGBA color_rgba = label.get_style_context().get_color(); diff --git a/main/src/ui/widgets/avatar_picture.vala b/main/src/ui/widgets/avatar_picture.vala new file mode 100644 index 00000000..e632413c --- /dev/null +++ b/main/src/ui/widgets/avatar_picture.vala @@ -0,0 +1,519 @@ +using Dino.Entities; +using Gtk; +using Xmpp; + +public class Dino.Ui.ViewModel.AvatarPictureTileModel : Object { + public string display_text { get; set; } + public Gdk.RGBA background_color { get; set; } + public File? image_file { get; set; } +} + +public class Dino.Ui.ViewModel.AvatarPictureModel : Object { + public ListModel tiles { get; set; } +} + +public class Dino.Ui.ViewModel.ConversationParticipantAvatarPictureTileModel : AvatarPictureTileModel { + private StreamInteractor stream_interactor; + private AvatarManager? avatar_manager { owned get { return stream_interactor == null ? null : stream_interactor.get_module(AvatarManager.IDENTITY); } } + private MucManager? muc_manager { owned get { return stream_interactor == null ? null : stream_interactor.get_module(MucManager.IDENTITY); } } + private Conversation? conversation; + private Jid? primary_avatar_jid; + private Jid? secondary_avatar_jid; + private Jid? display_name_jid; + + public ConversationParticipantAvatarPictureTileModel(StreamInteractor stream_interactor, Conversation conversation, Jid jid) { + this.stream_interactor = stream_interactor; + this.conversation = conversation; + this.primary_avatar_jid = jid; + this.display_name_jid = jid; + + string color_id = jid.to_string(); + if (conversation.type_ != Conversation.Type.CHAT && primary_avatar_jid.equals_bare(conversation.counterpart)) { + Jid? real_jid = muc_manager.get_real_jid(primary_avatar_jid, conversation.account); + if (real_jid != null && muc_manager.is_private_room(conversation.account, conversation.counterpart.bare_jid)) { + secondary_avatar_jid = primary_avatar_jid; + primary_avatar_jid = real_jid.bare_jid; + color_id = primary_avatar_jid.to_string(); + } else { + color_id = jid.resourcepart.to_string(); + } + } else if (conversation.type_ == Conversation.Type.CHAT) { + primary_avatar_jid = jid.bare_jid; + color_id = primary_avatar_jid.to_string(); + } + string display = Util.get_participant_display_name(stream_interactor, conversation, display_name_jid); + display_text = display.get_char(0).toupper().to_string(); + stream_interactor.get_module(RosterManager.IDENTITY).updated_roster_item.connect(on_roster_updated); + + float[] rgbf = color_id != null ? Xep.ConsistentColor.string_to_rgbf(color_id) : new float[] {0.5f, 0.5f, 0.5f}; + background_color = Gdk.RGBA() { red = rgbf[0], green = rgbf[1], blue = rgbf[2], alpha = 1.0f}; + + update_image_file(); + avatar_manager.received_avatar.connect(on_received_avatar); + avatar_manager.fetched_avatar.connect(on_received_avatar); + } + + private void update_image_file() { + File image_file = avatar_manager.get_avatar_file(conversation.account, primary_avatar_jid); + if (image_file == null && secondary_avatar_jid != null) { + image_file = avatar_manager.get_avatar_file(conversation.account, secondary_avatar_jid); + } + this.image_file = image_file; + } + + private void on_received_avatar(Jid jid, Account account) { + if (account.equals(conversation.account) && (jid.equals(primary_avatar_jid) || jid.equals(secondary_avatar_jid))) { + update_image_file(); + } + } + + private void on_roster_updated(Account account, Jid jid) { + if (account.equals(conversation.account) && jid.equals(display_name_jid)) { + string display = Util.get_participant_display_name(stream_interactor, conversation, display_name_jid); + display_text = display.get_char(0).toupper().to_string(); + } + } +} + +public class Dino.Ui.ViewModel.CompatAvatarPictureModel : AvatarPictureModel { + private StreamInteractor stream_interactor; + private AvatarManager? avatar_manager { owned get { return stream_interactor == null ? null : stream_interactor.get_module(AvatarManager.IDENTITY); } } + private MucManager? muc_manager { owned get { return stream_interactor == null ? null : stream_interactor.get_module(MucManager.IDENTITY); } } + private PresenceManager? presence_manager { owned get { return stream_interactor == null ? null : stream_interactor.get_module(PresenceManager.IDENTITY); } } + private ConnectionManager? connection_manager { owned get { return stream_interactor == null ? null : stream_interactor.connection_manager; } } + private Conversation? conversation; + + construct { + tiles = new GLib.ListStore(typeof(ViewModel.AvatarPictureTileModel)); + } + + public CompatAvatarPictureModel(StreamInteractor? stream_interactor) { + this.stream_interactor = stream_interactor; + if (stream_interactor != null) { + connect_signals_weak(this); + } + } + + private static void connect_signals_weak(CompatAvatarPictureModel model_) { + WeakRef model_weak = WeakRef(model_); + ulong muc_manager_private_room_occupant_updated_handler_id = 0; + ulong muc_manager_proom_info_updated_handler_id = 0; + ulong avatar_manager_received_avatar_handler_id = 0; + ulong avatar_manager_fetched_avatar_handler_id = 0; + muc_manager_private_room_occupant_updated_handler_id = model_.muc_manager.private_room_occupant_updated.connect((muc_manager, account, room, jid) => { + CompatAvatarPictureModel? model = (CompatAvatarPictureModel) model_weak.get(); + if (model != null) { + model.on_room_updated(account, room); + } else if (muc_manager_private_room_occupant_updated_handler_id != 0) { + muc_manager.disconnect(muc_manager_private_room_occupant_updated_handler_id); + muc_manager_private_room_occupant_updated_handler_id = 0; + } + }); + muc_manager_proom_info_updated_handler_id = model_.muc_manager.room_info_updated.connect((muc_manager, account, room) => { + CompatAvatarPictureModel? model = (CompatAvatarPictureModel) model_weak.get(); + if (model != null) { + model.on_room_updated(account, room); + } else if (muc_manager_proom_info_updated_handler_id != 0) { + muc_manager.disconnect(muc_manager_proom_info_updated_handler_id); + muc_manager_proom_info_updated_handler_id = 0; + } + }); + avatar_manager_received_avatar_handler_id = model_.avatar_manager.received_avatar.connect((avatar_manager, jid, account) => { + CompatAvatarPictureModel? model = (CompatAvatarPictureModel) model_weak.get(); + if (model != null) { + model.on_received_avatar(jid, account); + } else if (avatar_manager_received_avatar_handler_id != 0) { + avatar_manager.disconnect(avatar_manager_received_avatar_handler_id); + avatar_manager_received_avatar_handler_id = 0; + } + }); + avatar_manager_fetched_avatar_handler_id = model_.avatar_manager.fetched_avatar.connect((avatar_manager, jid, account) => { + CompatAvatarPictureModel? model = (CompatAvatarPictureModel) model_weak.get(); + if (model != null) { + model.on_received_avatar(jid, account); + } else if (avatar_manager_fetched_avatar_handler_id != 0) { + avatar_manager.disconnect(avatar_manager_fetched_avatar_handler_id); + avatar_manager_fetched_avatar_handler_id = 0; + } + }); + } + + private void on_room_updated(Account account, Jid room) { + if (conversation != null && account.equals(conversation.account) && conversation.counterpart.equals_bare(room)) { + reset(); + set_conversation(conversation); + } + } + + private void on_received_avatar(Jid jid, Account account) { + on_room_updated(account, jid); + } + + public void reset() { + (tiles as GLib.ListStore).remove_all(); + } + + public CompatAvatarPictureModel set_conversation(Conversation conversation) { + if (stream_interactor == null) { + critical("set_conversation() used on CompatAvatarPictureModel without stream_interactor"); + return this; + } + this.conversation = conversation; + if (conversation.type_ == Conversation.Type.GROUPCHAT) { + if (avatar_manager.has_avatar(conversation.account, conversation.counterpart)) { + add_internal("#", conversation.counterpart.to_string(), avatar_manager.get_avatar_file(conversation.account, conversation.counterpart)); + } else { + Gee.List? occupants = muc_manager.get_other_offline_members(conversation.counterpart, conversation.account); + if (occupants != null && !occupants.is_empty && muc_manager.is_private_room(conversation.account, conversation.counterpart)) { + int count = occupants.size > 4 ? 3 : occupants.size; + for (int i = 0; i < count; i++) { + add_participant(conversation, occupants[i]); + } + if (occupants.size > 4) { + add_internal("+"); + } + } else { + add_internal("#", conversation.counterpart.to_string()); + } + } + } else { + add_participant(conversation, conversation.counterpart); + } + return this; + } + + public CompatAvatarPictureModel add_participant(Conversation conversation, Jid jid) { + if (stream_interactor == null) { + critical("add_participant() used on CompatAvatarPictureModel without stream_interactor"); + return this; + } + (tiles as GLib.ListStore).append(new ConversationParticipantAvatarPictureTileModel(stream_interactor, conversation, jid)); + return this; + } + + public CompatAvatarPictureModel add(string display, string? color_id = null, File? image_file = null) { + add_internal(display, color_id, image_file); + return this; + } + + private AvatarPictureTileModel add_internal(string display, string? color_id = null, File? image_file = null) { + GLib.ListStore store = tiles as GLib.ListStore; + float[] rgbf = color_id != null ? Xep.ConsistentColor.string_to_rgbf(color_id) : new float[] {0.5f, 0.5f, 0.5f}; + var model = new ViewModel.AvatarPictureTileModel() { + display_text = display.get_char(0).toupper().to_string(), + background_color = Gdk.RGBA() { red = rgbf[0], green = rgbf[1], blue = rgbf[2], alpha = 1.0f}, + image_file = image_file + }; + store.append(model); + return model; + } +} + + +public class Dino.Ui.CompatAvatarDrawer { + public float radius_percent { get; set; default = 0.2f; } + public ViewModel.AvatarPictureModel? model { get; set; } + public int height_request { get; set; default = 35; } + public int width_request { get; set; default = 35; } + public string font_family { get; set; default = "Sans"; } + + public Cairo.ImageSurface draw_image_surface() { + Cairo.ImageSurface surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, width_request, height_request); + draw_on_context(new Cairo.Context(surface)); + return surface; + } + + public void draw_on_context(Cairo.Context ctx) { + double radius = (width_request + height_request) * 0.25f * radius_percent; + double degrees = Math.PI / 180.0; + ctx.new_sub_path(); + ctx.arc(width_request - radius, radius, radius, -90 * degrees, 0 * degrees); + ctx.arc(width_request - radius, height_request - radius, radius, 0 * degrees, 90 * degrees); + ctx.arc(radius, height_request - radius, radius, 90 * degrees, 180 * degrees); + ctx.arc(radius, radius, radius, 180 * degrees, 270 * degrees); + ctx.close_path(); + ctx.clip(); + + if (this.model.tiles.get_n_items() == 4) { + Cairo.Surface buffer = new Cairo.Surface.similar(ctx.get_target(), Cairo.Content.COLOR_ALPHA, width_request, height_request); + Cairo.Context bufctx = new Cairo.Context(buffer); + bufctx.scale(0.5, 0.5); + bufctx.set_source_surface(sub_surface_idx(ctx, 0, width_request - 1, height_request - 1, 2), 0, 0); + bufctx.paint(); + bufctx.set_source_surface(sub_surface_idx(ctx, 1, width_request - 1, height_request - 1, 2), width_request + 1, 0); + bufctx.paint(); + bufctx.set_source_surface(sub_surface_idx(ctx, 2, width_request - 1, height_request - 1, 2), 0, height_request + 1); + bufctx.paint(); + bufctx.set_source_surface(sub_surface_idx(ctx, 3, width_request - 1, height_request - 1, 2), width_request + 1, height_request + 1); + bufctx.paint(); + + ctx.set_source_surface(buffer, 0, 0); + ctx.paint(); + } else if (this.model.tiles.get_n_items() == 3) { + Cairo.Surface buffer = new Cairo.Surface.similar(ctx.get_target(), Cairo.Content.COLOR_ALPHA, width_request, height_request); + Cairo.Context bufctx = new Cairo.Context(buffer); + bufctx.scale(0.5, 0.5); + bufctx.set_source_surface(sub_surface_idx(ctx, 0, width_request - 1, height_request - 1, 2), 0, 0); + bufctx.paint(); + bufctx.set_source_surface(sub_surface_idx(ctx, 1, width_request - 1, height_request * 2, 2), width_request + 1, 0); + bufctx.paint(); + bufctx.set_source_surface(sub_surface_idx(ctx, 2, width_request - 1, height_request - 1, 2), 0, width_request + 1); + bufctx.paint(); + + ctx.set_source_surface(buffer, 0, 0); + ctx.paint(); + } else if (this.model.tiles.get_n_items() == 2) { + Cairo.Surface buffer = new Cairo.Surface.similar(ctx.get_target(), Cairo.Content.COLOR_ALPHA, width_request, height_request); + Cairo.Context bufctx = new Cairo.Context(buffer); + bufctx.scale(0.5, 0.5); + bufctx.set_source_surface(sub_surface_idx(ctx, 0, width_request - 1, height_request * 2, 2), 0, 0); + bufctx.paint(); + bufctx.set_source_surface(sub_surface_idx(ctx, 1, width_request - 1, height_request * 2, 2), width_request + 1, 0); + bufctx.paint(); + + ctx.set_source_surface(buffer, 0, 0); + ctx.paint(); + } else if (this.model.tiles.get_n_items() == 1) { + ctx.set_source_surface(sub_surface_idx(ctx, 0, width_request, height_request, 1), 0, 0); + ctx.paint(); + } else if (this.model.tiles.get_n_items() == 0) { + ctx.set_source_surface(sub_surface_idx(ctx, -1, width_request, height_request, 1), 0, 0); + ctx.paint(); + } + ctx.set_source_rgb(0, 0, 0); + } + + private Cairo.Surface sub_surface_idx(Cairo.Context ctx, int idx, int width, int height, int font_factor = 1) { + ViewModel.AvatarPictureTileModel tile = (ViewModel.AvatarPictureTileModel) this.model.tiles.get_item(idx); + Gdk.Pixbuf? avatar = new Gdk.Pixbuf.from_file(tile.image_file.get_path()); + string? name = idx >= 0 ? tile.display_text : ""; + Gdk.RGBA hex_color = tile.background_color; + return sub_surface(ctx, font_family, avatar, name, hex_color, width, height, font_factor); + } + + private static Cairo.Surface sub_surface(Cairo.Context ctx, string font_family, Gdk.Pixbuf? avatar, string? name, Gdk.RGBA background_color, int width, int height, int font_factor = 1) { + Cairo.Surface buffer = new Cairo.Surface.similar(ctx.get_target(), Cairo.Content.COLOR_ALPHA, width, height); + Cairo.Context bufctx = new Cairo.Context(buffer); + if (avatar == null) { + Gdk.cairo_set_source_rgba(bufctx, background_color); + bufctx.rectangle(0, 0, width, height); + bufctx.fill(); + + string text = name == null ? "…" : name.get_char(0).toupper().to_string(); + bufctx.select_font_face(font_family, Cairo.FontSlant.NORMAL, Cairo.FontWeight.NORMAL); + bufctx.set_font_size(width / font_factor < 40 ? font_factor * 17 : font_factor * 25); + Cairo.TextExtents extents; + bufctx.text_extents(text, out extents); + double x_pos = width/2 - (extents.width/2 + extents.x_bearing); + double y_pos = height/2 - (extents.height/2 + extents.y_bearing); + bufctx.move_to(x_pos, y_pos); + bufctx.set_source_rgba(1, 1, 1, 1); + bufctx.show_text(text); + } else { + double w_scale = (double) width / avatar.width; + double h_scale = (double) height / avatar.height; + double scale = double.max(w_scale, h_scale); + bufctx.scale(scale, scale); + + double x_off = 0, y_off = 0; + if (scale == h_scale) { + x_off = (width / scale - avatar.width) / 2.0; + } else { + y_off = (height / scale - avatar.height) / 2.0; + } + + Gdk.cairo_set_source_pixbuf(bufctx, avatar, x_off, y_off); + bufctx.get_source().set_filter(Cairo.Filter.BEST); + bufctx.paint(); + } + return buffer; + } +} + +public class Dino.Ui.AvatarPicture : Gtk.Widget { + public float radius_percent { get; set; default = 0.2f; } + public ViewModel.AvatarPictureModel? model { get; set; } + private Gee.List tiles = new Gee.ArrayList(); + + private ViewModel.AvatarPictureModel? old_model; + private ulong model_tiles_items_changed_handler; + + construct { + height_request = 35; + width_request = 35; + set_css_name("picture"); + add_css_class("avatar"); + notify["radius-percent"].connect(queue_draw); + notify["model"].connect(on_model_changed); + } + + private void on_model_changed() { + if (old_model != null) { + old_model.tiles.disconnect(model_tiles_items_changed_handler); + } + foreach (Tile tile in tiles) { + tile.unparent(); + tile.dispose(); + } + tiles.clear(); + old_model = model; + if (model != null) { + model_tiles_items_changed_handler = model.tiles.items_changed.connect(on_model_items_changed); + for(int i = 0; i < model.tiles.get_n_items(); i++) { + Tile tile = new Tile(); + tile.model = model.tiles.get_item(i) as ViewModel.AvatarPictureTileModel; + tile.insert_after(this, tiles.is_empty ? null : tiles.last()); + tiles.add(tile); + } + } + } + + private void on_model_items_changed(uint position, uint removed, uint added) { + while (removed > 0) { + Tile old = tiles.remove_at((int) position); + old.unparent(); + old.dispose(); + removed--; + } + while (added > 0) { + Tile tile = new Tile(); + tile.model = model.tiles.get_item(position) as ViewModel.AvatarPictureTileModel; + tile.insert_after(this, position == 0 ? null : tiles[(int) position - 1]); + tiles.insert((int) position, tile); + position++; + added--; + } + queue_allocate(); + } + + public override void measure(Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { + minimum_baseline = natural_baseline = -1; + if (orientation == Orientation.HORIZONTAL) { + minimum = natural = width_request; + } else { + minimum = natural = height_request; + } + } + + public override void size_allocate(int width, int height, int baseline) { + int half_width_size = width / 2; + int half_height_size = height / 2; + int half_width_offset = (width % 2 == 0) ? half_width_size : half_width_size + 1; + int half_height_offset = (height % 2 == 0) ? half_height_size : half_height_size + 1; + if (tiles.size == 1) { + tiles[0].allocate(width, height, baseline, null); + } else if (tiles.size == 2) { + tiles[0].allocate_size(Allocation() { x = 0, y = 0, width = half_width_size, height = height }, baseline); + tiles[1].allocate_size(Allocation() { x = half_width_offset, y = 0, width = half_width_size, height = height }, baseline); + } else if (tiles.size == 3) { + tiles[0].allocate_size(Allocation() { x = 0, y = 0, width = half_width_size, height = height }, baseline); + tiles[1].allocate_size(Allocation() { x = half_width_offset, y = 0, width = half_width_size, height = half_height_size }, baseline); + tiles[2].allocate_size(Allocation() { x = half_width_offset, y = half_height_offset, width = half_width_size, height = half_height_size }, baseline); + } else if (tiles.size == 4) { + tiles[0].allocate_size(Allocation() { x = 0, y = 0, width = half_width_size, height = half_height_size }, baseline); + tiles[1].allocate_size(Allocation() { x = half_width_offset, y = 0, width = half_width_size, height = half_height_size }, baseline); + tiles[2].allocate_size(Allocation() { x = 0, y = half_height_offset, width = half_width_size, height = half_height_size }, baseline); + tiles[3].allocate_size(Allocation() { x = half_width_offset, y = half_height_offset, width = half_width_size, height = half_height_size }, baseline); + } + } + + public override SizeRequestMode get_request_mode() { + return SizeRequestMode.CONSTANT_SIZE; + } + + public override void snapshot(Gtk.Snapshot snapshot) { + Graphene.Rect bounds = Graphene.Rect(); + bounds.init(0, 0, get_width(), get_height()); + Gsk.RoundedRect rounded_rect = Gsk.RoundedRect(); + rounded_rect.init_from_rect(bounds, (get_width() + get_height()) * 0.25f * radius_percent); + snapshot.push_rounded_clip(rounded_rect); + base.snapshot(snapshot); + snapshot.pop(); + } + + public override void dispose() { + model = null; + on_model_changed(); + base.dispose(); + } + + private class Tile : Gtk.Widget { + public ViewModel.AvatarPictureTileModel? model { get; set; } + public Gdk.RGBA background_color { get; set; default = Gdk.RGBA(){ red = 1.0f, green = 1.0f, blue = 1.0f, alpha = 0.0f }; } + public string display_text { get { return label.get_text(); } set { label.set_text(value); } } + public File? image_file { get { return picture.file; } set { picture.file = value; } } + + private Binding? background_color_binding; + private Binding? display_text_binding; + private Binding? image_file_binding; + + private Label label = new Label(""); + private Picture picture = new Picture(); + + construct { + label.insert_after(this, null); + label.attributes = new Pango.AttrList(); + label.attributes.insert(Pango.attr_foreground_new(uint16.MAX, uint16.MAX, uint16.MAX)); +#if GTK_4_8 && VALA_0_58 + picture.content_fit = Gtk.ContentFit.COVER; +#elif GTK_4_8 + picture.@set("content-fit", 2); +#endif + picture.insert_after(this, label); + this.notify["model"].connect(on_model_changed); + } + + private void on_model_changed() { + if (background_color_binding != null) background_color_binding.unbind(); + if (display_text_binding != null) display_text_binding.unbind(); + if (image_file_binding != null) image_file_binding.unbind(); + if (model != null) { + background_color_binding = model.bind_property("background-color", this, "background-color", BindingFlags.SYNC_CREATE); + display_text_binding = model.bind_property("display-text", this, "display-text", BindingFlags.SYNC_CREATE); + image_file_binding = model.bind_property("image-file", this, "image-file", BindingFlags.SYNC_CREATE); + } else { + background_color_binding = null; + display_text_binding = null; + image_file_binding = null; + } + } + + public override void dispose() { + if (background_color_binding != null) background_color_binding.unbind(); + if (display_text_binding != null) display_text_binding.unbind(); + if (image_file_binding != null) image_file_binding.unbind(); + background_color_binding = null; + display_text_binding = null; + image_file_binding = null; + label.unparent(); + picture.unparent(); + base.dispose(); + } + + public override void size_allocate(int width, int height, int baseline) { + int min, nat, bl_min, bl_nat; + picture.measure(Orientation.HORIZONTAL, -1, out min, out nat, out bl_min, out bl_nat); + if (nat > 0) { + picture.allocate(width, height, baseline, null); + label.visible = false; + } else { + picture.allocate(0, 0, 0, null); + label.attributes = new Pango.AttrList(); + label.attributes.insert(Pango.attr_foreground_new(uint16.MAX, uint16.MAX, uint16.MAX)); + label.attributes.insert(Pango.attr_scale_new(double.min((double)width, (double)height) * 0.05)); + label.margin_bottom = height/40; + label.visible = true; + label.allocate(width, height, baseline, null); + } + } + + public override void snapshot(Gtk.Snapshot snapshot) { + if (label.visible) { + Graphene.Rect bounds = Graphene.Rect(); + bounds.init(0, 0, get_width(), get_height()); + snapshot.append_node(new Gsk.ColorNode(background_color, bounds)); + } + base.snapshot(snapshot); + } + } +} \ No newline at end of file diff --git a/main/src/ui/widgets/date_separator.vala b/main/src/ui/widgets/date_separator.vala index 95729bce..b5d84a5b 100644 --- a/main/src/ui/widgets/date_separator.vala +++ b/main/src/ui/widgets/date_separator.vala @@ -40,8 +40,14 @@ public class Dino.Ui.ViewModel.CompatDateSeparatorModel : DateSeparatorModel { private void update_time_label() { date_label = get_relative_time(date); - time_update_timeout = Timeout.add_seconds((int) get_next_time_change(), () => { - if (time_update_timeout != 0) update_time_label(); + time_update_timeout = set_update_time_label_timeout((int) get_next_time_change(), this); + } + + private static uint set_update_time_label_timeout(int interval, CompatDateSeparatorModel model_) { + WeakRef model_weak = WeakRef(model_); + return Timeout.add_seconds(interval, () => { + CompatDateSeparatorModel? model = (CompatDateSeparatorModel) model_weak.get(); + if (model != null && model.time_update_timeout != 0) model.update_time_label(); return false; }); } diff --git a/meson.build b/meson.build new file mode 100644 index 00000000..aea22d57 --- /dev/null +++ b/meson.build @@ -0,0 +1,23 @@ +project('xmpp-vala', 'vala') + +fs = import('fs') +python = import('python') + +dep_gdk_pixbuf = dependency('gdk-pixbuf-2.0') +dep_gee = dependency('gee-0.8') +dep_gio = dependency('gio-2.0') +dep_glib = dependency('glib-2.0') +dep_gmodule = dependency('gmodule-2.0') +dep_gtk4 = dependency('gtk4') +dep_icu_uc = dependency('icu-uc') +dep_libadwaita = dependency('libadwaita-1') +dep_m = meson.get_compiler('c').find_library('m', required: false) +dep_sqlite3 = dependency('sqlite3', version: '>=3.24') + +prog_git = find_program('git', required: false) +prog_python = python.find_installation() + +subdir('qlite') +subdir('xmpp-vala') +subdir('libdino') +subdir('main') diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 00000000..6e47b7c8 --- /dev/null +++ b/meson_options.txt @@ -0,0 +1 @@ +option('plugindir', type: 'string', value: 'lib/dino/plugins', description: 'Plugin directory for Dino plugins') diff --git a/plugins/http-files/src/file_provider.vala b/plugins/http-files/src/file_provider.vala index 1433a74f..34c3a48a 100644 --- a/plugins/http-files/src/file_provider.vala +++ b/plugins/http-files/src/file_provider.vala @@ -10,13 +10,16 @@ public class FileProvider : Dino.FileProvider, Object { private StreamInteractor stream_interactor; private Dino.Database dino_db; + private Soup.Session session; private static Regex http_url_regex = /^https?:\/\/([^\s#]*)$/; // Spaces are invalid in URLs and we can't use fragments for downloads private static Regex omemo_url_regex = /^aesgcm:\/\/(.*)#(([A-Fa-f0-9]{2}){48}|([A-Fa-f0-9]{2}){44})$/; public FileProvider(StreamInteractor stream_interactor, Dino.Database dino_db) { this.stream_interactor = stream_interactor; this.dino_db = dino_db; + this.session = new Soup.Session(); + session.user_agent = @"Dino/$(Dino.get_short_version()) "; stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(new ReceivedMessageListener(this)); } @@ -66,7 +69,7 @@ public class FileProvider : Dino.FileProvider, Object { public bool is_readable() { if (!can_poll()) throw new IOError.NOT_SUPPORTED("Stream is not pollable"); - return ((PollableInputStream)inner).is_readable(); + return remaining_size <= 0 || ((PollableInputStream)inner).is_readable(); } private ssize_t check_limit(ssize_t read) throws IOError { @@ -114,8 +117,6 @@ public class FileProvider : Dino.FileProvider, Object { HttpFileReceiveData? http_receive_data = receive_data as HttpFileReceiveData; if (http_receive_data == null) return file_meta; - var session = new Soup.Session(); - session.user_agent = @"Dino/$(Dino.get_short_version()) "; var head_message = new Soup.Message("HEAD", http_receive_data.url); head_message.request_headers.append("Accept-Encoding", "identity"); @@ -150,8 +151,6 @@ public class FileProvider : Dino.FileProvider, Object { HttpFileReceiveData? http_receive_data = receive_data as HttpFileReceiveData; if (http_receive_data == null) assert(false); - var session = new Soup.Session(); - session.user_agent = @"Dino/$(Dino.get_short_version()) "; var get_message = new Soup.Message("GET", http_receive_data.url); try { diff --git a/plugins/http-files/src/file_sender.vala b/plugins/http-files/src/file_sender.vala index a39d695b..957611d0 100644 --- a/plugins/http-files/src/file_sender.vala +++ b/plugins/http-files/src/file_sender.vala @@ -7,12 +7,15 @@ namespace Dino.Plugins.HttpFiles { public class HttpFileSender : FileSender, Object { private StreamInteractor stream_interactor; private Database db; + private Soup.Session session; private HashMap max_file_sizes = new HashMap(Account.hash_func, Account.equals_func); public HttpFileSender(StreamInteractor stream_interactor, Database db) { this.stream_interactor = stream_interactor; this.db = db; + this.session = new Soup.Session(); + session.user_agent = @"Dino/$(Dino.get_short_version()) "; stream_interactor.stream_negotiated.connect(on_stream_negotiated); stream_interactor.get_module(MessageProcessor.IDENTITY).build_message_stanza.connect(check_add_oob); } @@ -90,8 +93,6 @@ public class HttpFileSender : FileSender, Object { Xmpp.XmppStream? stream = stream_interactor.get_stream(file_transfer.account); if (stream == null) return; - var session = new Soup.Session(); - session.user_agent = @"Dino/$(Dino.get_short_version()) "; var put_message = new Soup.Message("PUT", file_send_data.url_up); #if SOUP_3_0 put_message.set_request_body(file_meta.mime_type, file_transfer.input_stream, (ssize_t) file_meta.size); diff --git a/plugins/omemo/src/ui/bad_messages_populator.vala b/plugins/omemo/src/ui/bad_messages_populator.vala index 3cb3375b..8f087482 100644 --- a/plugins/omemo/src/ui/bad_messages_populator.vala +++ b/plugins/omemo/src/ui/bad_messages_populator.vala @@ -94,6 +94,7 @@ public class BadMessagesPopulator : Plugins.ConversationItemPopulator, Plugins.C foreach (BadMessageItem bad_item in bad_items) { item_collection.remove_item(bad_item); } + bad_items.clear(); } public void init(Conversation conversation, Plugins.ConversationItemCollection item_collection, Plugins.WidgetType type) { @@ -103,7 +104,9 @@ public class BadMessagesPopulator : Plugins.ConversationItemPopulator, Plugins.C init_state(); } - public void close(Conversation conversation) { } + public void close(Conversation conversation) { + clear_state(); + } public void populate_timespan(Conversation conversation, DateTime after, DateTime before) { } } @@ -131,9 +134,17 @@ public class BadMessageItem : Plugins.MetaConversationItem { } public class BadMessagesWidget : Box { + private Plugin plugin; + private Conversation conversation; + private Jid jid; + private Label label; + public BadMessagesWidget(Plugin plugin, Conversation conversation, Jid jid, BadnessType badness_type) { Object(orientation:Orientation.HORIZONTAL, spacing:5); + this.plugin = plugin; + this.conversation = conversation; + this.jid = jid; this.halign = Align.CENTER; this.visible = true; @@ -159,19 +170,29 @@ public class BadMessagesWidget : Box { } else { warning_text += _("%s does not trust this device. That means, you might be missing messages.").printf(who); } - Label label = new Label(warning_text) { margin_start=70, margin_end=70, justify=Justification.CENTER, use_markup=true, selectable=true, wrap=true, wrap_mode=Pango.WrapMode.WORD_CHAR, hexpand=true }; + label = new Label(warning_text) { margin_start=70, margin_end=70, justify=Justification.CENTER, use_markup=true, selectable=true, wrap=true, wrap_mode=Pango.WrapMode.WORD_CHAR, hexpand=true }; label.add_css_class("dim-label"); this.append(label); - label.activate_link.connect(() => { - if (badness_type == BadnessType.UNTRUSTED) { - ContactDetailsDialog dialog = new ContactDetailsDialog(plugin, conversation.account, jid); - dialog.set_transient_for((Window) get_root()); - dialog.present(); - } + if (badness_type == BadnessType.UNTRUSTED) { + label.activate_link.connect(on_label_activate_link); + } + } - return false; - }); + private bool on_label_activate_link() { + ContactDetailsDialog dialog = new ContactDetailsDialog(plugin, conversation.account, jid); + dialog.set_transient_for((Window) get_root()); + dialog.present(); + return false; + } + + public override void dispose() { + if (label != null) { + label.unparent(); + label.dispose(); + label = null; + } + base.dispose(); } } diff --git a/plugins/rtp/src/video_widget.vala b/plugins/rtp/src/video_widget.vala index af2a9f1d..0d66476b 100644 --- a/plugins/rtp/src/video_widget.vala +++ b/plugins/rtp/src/video_widget.vala @@ -1,4 +1,5 @@ private static extern unowned Gst.Video.Info gst_video_frame_get_video_info(Gst.Video.Frame frame); +[CCode (array_length_type = "size_t", type = "void*")] private static extern unowned uint8[] gst_video_frame_get_data(Gst.Video.Frame frame); public class Dino.Plugins.Rtp.Paintable : Gdk.Paintable, Object { diff --git a/qlite/meson.build b/qlite/meson.build new file mode 100644 index 00000000..714a4224 --- /dev/null +++ b/qlite/meson.build @@ -0,0 +1,22 @@ +dependencies = [ + dep_gee, + dep_glib, + dep_sqlite3, +] +sources = files( + 'src/column.vala', + 'src/database.vala', + 'src/delete_builder.vala', + 'src/insert_builder.vala', + 'src/query_builder.vala', + 'src/row.vala', + 'src/statement_builder.vala', + 'src/table.vala', + 'src/update_builder.vala', + 'src/upsert_builder.vala', +) +c_args = [ + '-DG_LOG_DOMAIN="qlite"', +] +lib_qlite = library('qlite', sources, c_args: c_args, vala_args: ['--vapidir', meson.current_source_dir() / 'vapi'], dependencies: dependencies) +dep_qlite = declare_dependency(link_with: lib_qlite, include_directories: include_directories('.')) diff --git a/xmpp-vala/CMakeLists.txt b/xmpp-vala/CMakeLists.txt index a988a088..39c090fe 100644 --- a/xmpp-vala/CMakeLists.txt +++ b/xmpp-vala/CMakeLists.txt @@ -106,6 +106,9 @@ SOURCES "src/module/xep/0384_omemo/omemo_encryptor.vala" "src/module/xep/0384_omemo/omemo_decryptor.vala" + "src/module/xep/0392_consistent_color/consistent_color.vala" + "src/module/xep/0392_consistent_color/hsluv.vala" + "src/module/xep/0184_message_delivery_receipts.vala" "src/module/xep/0191_blocking_command.vala" "src/module/xep/0198_stream_management.vala" @@ -160,7 +163,7 @@ DEPENDS add_definitions(${VALA_CFLAGS} -DG_LOG_DOMAIN="xmpp-vala") add_library(xmpp-vala SHARED ${ENGINE_VALA_C}) add_dependencies(xmpp-vala xmpp-vala-vapi) -target_link_libraries(xmpp-vala ${ENGINE_PACKAGES}) +target_link_libraries(xmpp-vala ${ENGINE_PACKAGES} m) set_target_properties(xmpp-vala PROPERTIES VERSION 0.1 SOVERSION 0) install(TARGETS xmpp-vala ${TARGET_INSTALL}) @@ -175,6 +178,7 @@ if(BUILD_TESTS) "tests/jid.vala" "tests/stanza.vala" + "tests/color.vala" "tests/util.vala" CUSTOM_VAPIS ${CMAKE_BINARY_DIR}/exports/xmpp-vala_internal.vapi diff --git a/xmpp-vala/meson.build b/xmpp-vala/meson.build new file mode 100644 index 00000000..3064339a --- /dev/null +++ b/xmpp-vala/meson.build @@ -0,0 +1,133 @@ +dependencies = [ + dep_gdk_pixbuf, + dep_gee, + dep_gio, + dep_glib, + dep_icu_uc, + dep_m, +] +sources = files( + 'src/core/direct_tls_xmpp_stream.vala', + 'src/core/io_xmpp_stream.vala', + 'src/core/module_flag.vala', + 'src/core/namespace_state.vala', + 'src/core/stanza_attribute.vala', + 'src/core/stanza_node.vala', + 'src/core/stanza_reader.vala', + 'src/core/stanza_writer.vala', + 'src/core/starttls_xmpp_stream.vala', + 'src/core/stream_connect.vala', + 'src/core/tls_xmpp_stream.vala', + 'src/core/xmpp_log.vala', + 'src/core/xmpp_stream.vala', + 'src/glib_fixes.vapi', + 'src/module/bind.vala', + 'src/module/bookmarks_provider.vala', + 'src/module/conference.vala', + 'src/module/iq/module.vala', + 'src/module/iq/stanza.vala', + 'src/module/jid.vala', + 'src/module/message/module.vala', + 'src/module/message/stanza.vala', + 'src/module/presence/flag.vala', + 'src/module/presence/module.vala', + 'src/module/presence/stanza.vala', + 'src/module/roster/flag.vala', + 'src/module/roster/item.vala', + 'src/module/roster/module.vala', + 'src/module/roster/versioning_module.vala', + 'src/module/sasl.vala', + 'src/module/session.vala', + 'src/module/stanza.vala', + 'src/module/stanza_error.vala', + 'src/module/stream_error.vala', + 'src/module/util.vala', + 'src/module/xep/0004_data_forms.vala', + 'src/module/xep/0030_service_discovery/flag.vala', + 'src/module/xep/0030_service_discovery/identity.vala', + 'src/module/xep/0030_service_discovery/info_result.vala', + 'src/module/xep/0030_service_discovery/item.vala', + 'src/module/xep/0030_service_discovery/items_result.vala', + 'src/module/xep/0030_service_discovery/module.vala', + 'src/module/xep/0045_muc/flag.vala', + 'src/module/xep/0045_muc/module.vala', + 'src/module/xep/0045_muc/status_code.vala', + 'src/module/xep/0047_in_band_bytestreams.vala', + 'src/module/xep/0048_bookmarks.vala', + 'src/module/xep/0048_conference.vala', + 'src/module/xep/0049_private_xml_storage.vala', + 'src/module/xep/0054_vcard/module.vala', + 'src/module/xep/0059_result_set_management.vala', + 'src/module/xep/0060_pubsub.vala', + 'src/module/xep/0065_socks5_bytestreams.vala', + 'src/module/xep/0066_out_of_band_data.vala', + 'src/module/xep/0077_in_band_registration.vala', + 'src/module/xep/0082_date_time_profiles.vala', + 'src/module/xep/0084_user_avatars.vala', + 'src/module/xep/0085_chat_state_notifications.vala', + 'src/module/xep/0115_entity_capabilities.vala', + 'src/module/xep/0166_jingle/component.vala', + 'src/module/xep/0166_jingle/content.vala', + 'src/module/xep/0166_jingle/content_description.vala', + 'src/module/xep/0166_jingle/content_node.vala', + 'src/module/xep/0166_jingle/content_security.vala', + 'src/module/xep/0166_jingle/content_transport.vala', + 'src/module/xep/0166_jingle/jingle_flag.vala', + 'src/module/xep/0166_jingle/jingle_module.vala', + 'src/module/xep/0166_jingle/jingle_structs.vala', + 'src/module/xep/0166_jingle/reason_element.vala', + 'src/module/xep/0166_jingle/session.vala', + 'src/module/xep/0166_jingle/session_info.vala', + 'src/module/xep/0167_jingle_rtp/content_parameters.vala', + 'src/module/xep/0167_jingle_rtp/content_type.vala', + 'src/module/xep/0167_jingle_rtp/jingle_rtp_module.vala', + 'src/module/xep/0167_jingle_rtp/payload_type.vala', + 'src/module/xep/0167_jingle_rtp/session_info_type.vala', + 'src/module/xep/0167_jingle_rtp/stream.vala', + 'src/module/xep/0176_jingle_ice_udp/candidate.vala', + 'src/module/xep/0176_jingle_ice_udp/jingle_ice_udp_module.vala', + 'src/module/xep/0176_jingle_ice_udp/transport_parameters.vala', + 'src/module/xep/0177_jingle_raw_udp.vala', + 'src/module/xep/0184_message_delivery_receipts.vala', + 'src/module/xep/0191_blocking_command.vala', + 'src/module/xep/0198_stream_management.vala', + 'src/module/xep/0199_ping.vala', + 'src/module/xep/0203_delayed_delivery.vala', + 'src/module/xep/0215_external_service_discovery.vala', + 'src/module/xep/0234_jingle_file_transfer.vala', + 'src/module/xep/0249_direct_muc_invitations.vala', + 'src/module/xep/0260_jingle_socks5_bytestreams.vala', + 'src/module/xep/0261_jingle_in_band_bytestreams.vala', + 'src/module/xep/0272_muji.vala', + 'src/module/xep/0280_message_carbons.vala', + 'src/module/xep/0297_stanza_forwarding.vala', + 'src/module/xep/0298_coin.vala', + 'src/module/xep/0308_last_message_correction.vala', + 'src/module/xep/0313_2_message_archive_management.vala', + 'src/module/xep/0313_message_archive_management.vala', + 'src/module/xep/0333_chat_markers.vala', + 'src/module/xep/0334_message_processing_hints.vala', + 'src/module/xep/0353_call_invite_message.vala', + 'src/module/xep/0353_jingle_message_initiation.vala', + 'src/module/xep/0359_unique_stable_stanza_ids.vala', + 'src/module/xep/0363_http_file_upload.vala', + 'src/module/xep/0380_explicit_encryption.vala', + 'src/module/xep/0384_omemo/omemo_decryptor.vala', + 'src/module/xep/0384_omemo/omemo_encryptor.vala', + 'src/module/xep/0391_jingle_encrypted_transports.vala', + 'src/module/xep/0392_consistent_color/consistent_color.vala', + 'src/module/xep/0392_consistent_color/hsluv.vala', + 'src/module/xep/0402_bookmarks2.vala', + 'src/module/xep/0410_muc_self_ping.vala', + 'src/module/xep/0421_occupant_ids.vala', + 'src/module/xep/0428_fallback_indication.vala', + 'src/module/xep/0444_reactions.vala', + 'src/module/xep/0461_replies.vala', + 'src/module/xep/pixbuf_storage.vala', + 'src/util.vala', +) +c_args = [ + '-DG_LOG_DOMAIN="xmpp-vala"', +] +lib_xmpp_vala = library('xmpp-vala', sources, c_args: c_args, vala_args: ['--vapidir', meson.current_source_dir() / 'vapi'], dependencies: dependencies) +dep_xmpp_vala = declare_dependency(link_with: lib_xmpp_vala, include_directories: include_directories('.')) diff --git a/xmpp-vala/src/module/xep/0085_chat_state_notifications.vala b/xmpp-vala/src/module/xep/0085_chat_state_notifications.vala index fa0360c0..7634e605 100644 --- a/xmpp-vala/src/module/xep/0085_chat_state_notifications.vala +++ b/xmpp-vala/src/module/xep/0085_chat_state_notifications.vala @@ -16,14 +16,12 @@ public class Module : XmppStreamModule { public signal void chat_state_received(XmppStream stream, Jid jid, string state, MessageStanza stanza); - private SendPipelineListener send_pipeline_listener = new SendPipelineListener(); - /** * "A message stanza that does not contain standard messaging content [...] SHOULD be a state other than " (0085, 5.6) */ public void send_state(XmppStream stream, Jid jid, string message_type, string state) { MessageStanza message = new MessageStanza() { to=jid, type_=message_type }; - message.stanza.put_node(new StanzaNode.build(state, NS_URI).add_self_xmlns()); + add_state_to_message(message, state); MessageProcessingHints.set_message_hint(message, MessageProcessingHints.HINT_NO_STORE); @@ -32,14 +30,12 @@ public class Module : XmppStreamModule { public override void attach(XmppStream stream) { stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI); - stream.get_module(MessageModule.IDENTITY).send_pipeline.connect(send_pipeline_listener); stream.get_module(MessageModule.IDENTITY).received_message.connect(on_received_message); } public override void detach(XmppStream stream) { stream.get_module(ServiceDiscovery.Module.IDENTITY).remove_feature(stream, NS_URI); stream.get_module(MessageModule.IDENTITY).received_message.disconnect(on_received_message); - stream.get_module(MessageModule.IDENTITY).send_pipeline.disconnect(send_pipeline_listener); } public override string get_ns() { return NS_URI; } @@ -57,19 +53,8 @@ public class Module : XmppStreamModule { } } -public class SendPipelineListener : StanzaListener { - - private string[] after_actions_const = {"MODIFY_BODY"}; - - public override string action_group { get { return "ADD_NODES"; } } - public override string[] after_actions { get { return after_actions_const; } } - - public override async bool run(XmppStream stream, MessageStanza message) { - if (message.body == null) return false; - if (message.type_ != MessageStanza.TYPE_CHAT) return false; - message.stanza.put_node(new StanzaNode.build(STATE_ACTIVE, NS_URI).add_self_xmlns()); - return false; - } +public static void add_state_to_message(MessageStanza message, string state) { + message.stanza.put_node(new StanzaNode.build(state, NS_URI).add_self_xmlns()); } } diff --git a/xmpp-vala/src/module/xep/0280_message_carbons.vala b/xmpp-vala/src/module/xep/0280_message_carbons.vala index 5b2dcb78..f7863188 100644 --- a/xmpp-vala/src/module/xep/0280_message_carbons.vala +++ b/xmpp-vala/src/module/xep/0280_message_carbons.vala @@ -58,6 +58,10 @@ public class ReceivedPipelineListener : StanzaListener { warning("Received alleged carbon message from %s, ignoring", message.from.to_string()); return true; } + if (message_node == null) { + warning("Received a carbon message with no message subnode in jabber:client namespace from %s, ignoring", message.from.to_string()); + return true; + } if (received_node != null) { message.add_flag(new MessageFlag(MessageFlag.TYPE_RECEIVED)); } else if (sent_node != null) { diff --git a/xmpp-vala/src/module/xep/0313_message_archive_management.vala b/xmpp-vala/src/module/xep/0313_message_archive_management.vala index 1caa1bc3..2235e118 100644 --- a/xmpp-vala/src/module/xep/0313_message_archive_management.vala +++ b/xmpp-vala/src/module/xep/0313_message_archive_management.vala @@ -11,8 +11,8 @@ public class QueryResult { public bool error { get; set; default=false; } public bool malformed { get; set; default=false; } public bool complete { get; set; default=false; } - public string first { get; set; } - public string last { get; set; } + public string? first { get; set; } + public string? last { get; set; } } public class Module : XmppStreamModule { @@ -65,16 +65,17 @@ public class Module : XmppStreamModule { } StanzaNode query_node = new StanzaNode.build("query", NS_VER(stream)).add_self_xmlns().put_node(data_form.get_submit_node()); - if (queryid != null) { - query_node.put_attribute("queryid", queryid); - } + query_node.put_attribute("queryid", queryid); return query_node; } internal async QueryResult query_archive(XmppStream stream, string ns, Jid? mam_server, StanzaNode query_node, Cancellable? cancellable = null) { - var res = new QueryResult(); - if (stream.get_flag(Flag.IDENTITY) == null) { res.error = true; return res; } + var res = new QueryResult(); + Flag? flag = stream.get_flag(Flag.IDENTITY); + string? query_id = query_node.get_attribute("queryid"); + if (flag == null || query_id == null) { res.error = true; return res; } + flag.active_query_ids.add(query_id); // Build and send query Iq.Stanza iq = new Iq.Stanza.set(query_node) { to=mam_server }; @@ -93,6 +94,11 @@ public class Module : XmppStreamModule { if ((res.first == null) != (res.last == null)) { res.malformed = true; return res; } res.complete = fin_node.get_attribute_bool("complete", false, ns); + Idle.add(() => { + flag.active_query_ids.remove(query_id); + return Source.REMOVE; + }, Priority.LOW); + return res; } @@ -104,7 +110,8 @@ public class ReceivedPipelineListener : StanzaListener { public override string[] after_actions { get { return after_actions_const; } } public override async bool run(XmppStream stream, MessageStanza message) { - if (stream.get_flag(Flag.IDENTITY) == null) return false; + Flag? flag = stream.get_flag(Flag.IDENTITY); + if (flag == null) return false; StanzaNode? message_node = message.stanza.get_deep_subnode(NS_VER(stream) + ":result", StanzaForwarding.NS_URI + ":forwarded", Xmpp.NS_URI + ":message"); if (message_node != null) { @@ -112,6 +119,28 @@ public class ReceivedPipelineListener : StanzaListener { DateTime? datetime = DelayedDelivery.get_time_for_node(forward_node); string? mam_id = message.stanza.get_deep_attribute(NS_VER(stream) + ":result", NS_VER(stream) + ":id"); string? query_id = message.stanza.get_deep_attribute(NS_VER(stream) + ":result", NS_VER(stream) + ":queryid"); + + if (query_id == null) { + warning("Received MAM message without queryid from %s, ignoring", message.from.to_string()); + return true; + } + + if (!flag.active_query_ids.contains(query_id)) { + warning("Received MAM message from %s with unknown query id %s, ignoring", message.from.to_string(), query_id ?? ""); + return true; + } + Jid? inner_from = null; + try { + inner_from = new Jid(message_node.get_attribute("from")); + } catch (InvalidJidError e) { + warning("Received MAM message with invalid from attribute in forwarded message from %s, ignoring", message.from.to_string()); + return true; + } + if (!message.from.equals(stream.get_flag(Bind.Flag.IDENTITY).my_jid.bare_jid) && !message.from.equals_bare(inner_from)) { + warning("Received MAM message from %s illegally impersonating %s, ignoring", message.from.to_string(), inner_from.to_string()); + return true; + } + message.add_flag(new MessageFlag(message.from, datetime, mam_id, query_id)); message.stanza = message_node; @@ -124,6 +153,7 @@ public class ReceivedPipelineListener : StanzaListener { public class Flag : XmppStreamFlag { public static FlagIdentity IDENTITY = new FlagIdentity(NS_URI, "message_archive_management"); public bool cought_up { get; set; default=false; } + public Gee.Set active_query_ids { get; set; default = new HashSet(); } public string ns_ver; public Flag(string ns_ver) { diff --git a/xmpp-vala/src/module/xep/0392_consistent_color/consistent_color.vala b/xmpp-vala/src/module/xep/0392_consistent_color/consistent_color.vala new file mode 100644 index 00000000..9ab7e4bb --- /dev/null +++ b/xmpp-vala/src/module/xep/0392_consistent_color/consistent_color.vala @@ -0,0 +1,37 @@ +namespace Xmpp.Xep.ConsistentColor { +private const double KR = 0.299; +private const double KG = 0.587; +private const double KB = 0.114; +private const double Y = 0.732; + +public float string_to_angle(string s) { + Checksum checksum = new Checksum(ChecksumType.SHA1); + checksum.update(s.data, -1); + size_t len = 20; + uint8[] digest = new uint8[len]; + checksum.get_digest(digest, ref len); + uint16 output = ((uint16)(*(uint16*)digest)).to_little_endian(); + return (((float) output) / 65536.0f) * 360.0f; +} + +private uint8[] rgbd_to_rgb(double[] rgbd) { + return {(uint8)(rgbd[0] * 255.0), (uint8)(rgbd[1] * 255.0), (uint8)(rgbd[2] * 255.0)}; +} + +private float[] rgbd_to_rgbf(double[] rgbd) { + return {(float)rgbd[0], (float)rgbd[1], (float)rgbd[2]}; +} + +private double[] angle_to_rgbd(double angle) { + return Hsluv.hsluv_to_rgb(new double[] {angle, 100, 50}); +} + +public float[] string_to_rgbf(string s) { + return rgbd_to_rgbf(angle_to_rgbd(string_to_angle(s))); +} + +public uint8[] string_to_rgb(string s) { + return rgbd_to_rgb(angle_to_rgbd(string_to_angle(s))); +} + +} diff --git a/xmpp-vala/src/module/xep/0392_consistent_color/hsluv.vala b/xmpp-vala/src/module/xep/0392_consistent_color/hsluv.vala new file mode 100644 index 00000000..b45d84de --- /dev/null +++ b/xmpp-vala/src/module/xep/0392_consistent_color/hsluv.vala @@ -0,0 +1,393 @@ +namespace Hsluv { + +private const double[] M0 = { 3.240969941904521, -1.537383177570093, -0.498610760293 }; +private const double[] M1 = { -0.96924363628087, 1.87596750150772, 0.041555057407175 }; +private const double[] M2 = { 0.055630079696993, -0.20397695888897, 1.056971514242878 }; + +private const double[] MInv0 = { 0.41239079926595, 0.35758433938387, 0.18048078840183 }; +private const double[] MInv1 = { 0.21263900587151, 0.71516867876775, 0.072192315360733 }; +private const double[] MInv2 = { 0.019330818715591, 0.11919477979462, 0.95053215224966 }; + +private double RefX = 0.95045592705167; +private double RefY = 1.0; +private double RefZ = 1.089057750759878; + +private double RefU = 0.19783000664283; +private double RefV = 0.46831999493879; + +private double Kappa = 903.2962962; +private double Epsilon = 0.0088564516; + +private struct Bounds { + double t0; + double t1; +} + +private Bounds get_bounds_sub(double L, double sub1, double sub2, int t, double[] m) { + double m1 = m[0]; + double m2 = m[1]; + double m3 = m[2]; + double top1 = (284517 * m1 - 94839 * m3) * sub2; + double top2 = (838422 * m3 + 769860 * m2 + 731718 * m1) * L * sub2 - 769860 * t * L; + double bottom = (632260 * m3 - 126452 * m2) * sub2 + 126452 * t; + return { top1 / bottom, top2 / bottom }; +} + +private Bounds[] get_bounds(double L) { + double sub1 = Math.pow(L + 16, 3) / 1560896; + double sub2 = sub1 > Epsilon ? sub1 : L / Kappa; + + return { + get_bounds_sub(L, sub1, sub2, 0, M0), + get_bounds_sub(L, sub1, sub2, 1, M0), + get_bounds_sub(L, sub1, sub2, 0, M1), + get_bounds_sub(L, sub1, sub2, 1, M1), + get_bounds_sub(L, sub1, sub2, 0, M2), + get_bounds_sub(L, sub1, sub2, 1, M2) + }; +} + +private double intersect_line_line(double[] lineA, double[] lineB) { + return (lineA[1] - lineB[1]) / (lineB[0] - lineA[0]); +} + +private double distance_from_pole(double[] point) { + return Math.sqrt(Math.pow(point[0], 2) + Math.pow(point[1], 2)); +} + +private bool length_of_ray_until_intersect(double theta, Bounds line, out double length) { + length = line.t1 / (Math.sin(theta) - line.t0 * Math.cos(theta)); + + return length >= 0; +} + +private double max_safe_chroma_for_l(double L) { + Bounds[] bounds = get_bounds(L); + double min = double.MAX; + + for (int i = 0; i < 2; ++i) { + var m1 = bounds[i].t0; + var b1 = bounds[i].t1; + var line = new double[] { m1, b1 }; + + double x = intersect_line_line(line, new double[] {-1 / m1, 0 }); + double length = distance_from_pole(new double[] { x, b1 + x * m1 }); + + min = double.min(min, length); + } + + return min; +} + +private double max_chroma_for_lh(double L, double H) { + double hrad = H / 360 * Math.PI * 2; + + Bounds[] bounds = get_bounds(L); + double min = double.MAX; + + foreach (var bound in bounds) { + double length; + + if (length_of_ray_until_intersect(hrad, bound, out length)) { + min = double.min(min, length); + } + } + + return min; +} + +private double dot_product(double[] a, double[] b) { + double sum = 0; + + for (int i = 0; i < a.length; ++i) { + sum += a[i] * b[i]; + } + + return sum; +} + +private double round(double value, int places) { + double n = Math.pow(10, places); + + return Math.round(value * n) / n; +} + +private double from_linear(double c) { + if (c <= 0.0031308) { + return 12.92 * c; + } else { + return 1.055 * Math.pow(c, 1 / 2.4) - 0.055; + } +} + +private double to_linear(double c) { + if (c > 0.04045) { + return Math.pow((c + 0.055) / (1 + 0.055), 2.4); + } else { + return c / 12.92; + } +} + +private int[] rgb_prepare(double[] tuple) { + for (int i = 0; i < tuple.length; ++i) { + tuple[i] = round(tuple[i], 3); + } + + for (int i = 0; i < tuple.length; ++i) { + double ch = tuple[i]; + + if (ch < -0.0001 || ch > 1.0001) { + return null; //throw new Error("Illegal rgb value: " + ch); + } + } + + var results = new int[tuple.length]; + + for (int i = 0; i < tuple.length; ++i) { + results[i] = (int) Math.round(tuple[i] * 255); + } + + return results; +} + +internal double[] xyz_to_rgb(double[] tuple) { + return new double[] { + from_linear(dot_product(M0, tuple)), + from_linear(dot_product(M1, tuple)), + from_linear(dot_product(M2, tuple)) + }; +} + +internal double[] rgb_to_xyz(double[] tuple) { + var rgbl = new double[] { + to_linear(tuple[0]), + to_linear(tuple[1]), + to_linear(tuple[2]) + }; + + return new double[] { + dot_product(MInv0, rgbl), + dot_product(MInv1, rgbl), + dot_product(MInv2, rgbl) + }; +} + +private double y_to_l(double Y) { + if (Y <= Epsilon) { + return (Y / RefY) * Kappa; + } else { + return 116 * Math.pow(Y / RefY, 1.0 / 3.0) - 16; + } +} + +private double l_to_y(double L) { + if (L <= 8) { + return RefY * L / Kappa; + } else { + return RefY * Math.pow((L + 16) / 116, 3); + } +} + +internal double[] xyz_to_luv(double[] tuple) { + double X = tuple[0]; + double Y = tuple[1]; + double Z = tuple[2]; + + double varU = (4 * X) / (X + (15 * Y) + (3 * Z)); + double varV = (9 * Y) / (X + (15 * Y) + (3 * Z)); + + double L = y_to_l(Y); + + if (L == 0) { + return new double[] { 0, 0, 0 }; + } + + var U = 13 * L * (varU - RefU); + var V = 13 * L * (varV - RefV); + + return new double [] { L, U, V }; +} + +internal double[] luv_to_xyz(double[] tuple) { + double L = tuple[0]; + double U = tuple[1]; + double V = tuple[2]; + + if (L == 0) { + return new double[] { 0, 0, 0 }; + } + + double varU = U / (13 * L) + RefU; + double varV = V / (13 * L) + RefV; + + double Y = l_to_y(L); + double X = 0 - (9 * Y * varU) / ((varU - 4) * varV - varU * varV); + double Z = (9 * Y - (15 * varV * Y) - (varV * X)) / (3 * varV); + + return new double[] { X, Y, Z }; +} + +internal double[] luv_to_lch(double[] tuple) { + double L = tuple[0]; + double U = tuple[1]; + double V = tuple[2]; + + double C = Math.pow(Math.pow(U, 2) + Math.pow(V, 2), 0.5); + double Hrad = Math.atan2(V, U); + + double H = Hrad * 180.0 / Math.PI; + + if (H < 0) { + H = 360 + H; + } + + return new double[] { L, C, H }; +} + +internal double[] lch_to_luv(double[] tuple) { + double L = tuple[0]; + double C = tuple[1]; + double H = tuple[2]; + + double Hrad = H / 360.0 * 2 * Math.PI; + double U = Math.cos(Hrad) * C; + double V = Math.sin(Hrad) * C; + + return new double [] { L, U, V }; +} + +internal double[] hsluv_to_lch(double[] tuple) { + double H = tuple[0]; + double S = tuple[1]; + double L = tuple[2]; + + if (L > 99.9999999) { + return new double[] { 100, 0, H }; + } + + if (L < 0.00000001) { + return new double[] { 0, 0, H }; + } + + double max = max_chroma_for_lh(L, H); + double C = max / 100 * S; + + return new double[] { L, C, H }; +} + +internal double[] lch_to_hsluv(double[] tuple) { + double L = tuple[0]; + double C = tuple[1]; + double H = tuple[2]; + + if (L > 99.9999999) { + return new double[] { H, 0, 100 }; + } + + if (L < 0.00000001) { + return new double[] { H, 0, 0 }; + } + + double max = max_chroma_for_lh(L, H); + double S = C / max * 100; + + return new double[] { H, S, L }; +} + +internal double[] hpluv_to_lch(double[] tuple) { + double H = tuple[0]; + double S = tuple[1]; + double L = tuple[2]; + + if (L > 99.9999999) { + return new double[] { 100, 0, H }; + } + + if (L < 0.00000001) { + return new double[] { 0, 0, H }; + } + + double max = max_safe_chroma_for_l(L); + double C = max / 100 * S; + + return new double[] { L, C, H }; +} + +internal double[] lch_to_hpluv(double[] tuple) { + double L = tuple[0]; + double C = tuple[1]; + double H = tuple[2]; + + if (L > 99.9999999) { + return new double[] { H, 0, 100 }; + } + + if (L < 0.00000001) { + return new double[] { H, 0, 0 }; + } + + double max = max_safe_chroma_for_l(L); + double S = C / max * 100; + + return new double[] { H, S, L }; +} + +internal string rgb_to_hex(double[] tuple) { + int[] prepared = rgb_prepare(tuple); + + return "#%.2x%.2x%.2x".printf(prepared[0], prepared[1], prepared[2]); +} + +internal double[] hex_to_tgb(string hex) { + return new double[] { + hex.substring(1, 2).to_long(null, 16) / 255.0, + hex.substring(3, 2).to_long(null, 16) / 255.0, + hex.substring(5, 2).to_long(null, 16) / 255.0 + }; +} + +internal double[] lch_to_rgb(double[] tuple) { + return xyz_to_rgb(luv_to_xyz(lch_to_luv(tuple))); +} + +internal double[] rgb_to_lch(double[] tuple) { + return luv_to_lch(xyz_to_luv(rgb_to_xyz(tuple))); +} + +// Rgb <--> Hsluv(p) + +internal double[] hsluv_to_rgb(double[] tuple) { + return lch_to_rgb(hsluv_to_lch(tuple)); +} + +internal double[] rgb_to_hsluv(double[] tuple) { + return lch_to_hsluv(rgb_to_lch(tuple)); +} + +internal double[] hpluv_to_rgb(double[] tuple) { + return lch_to_rgb(hpluv_to_lch(tuple)); +} + +internal double[] rgb_to_hpluv(double[] tuple) { + return lch_to_hpluv(rgb_to_lch(tuple)); +} + +// Hex + +internal string hsluv_to_hex(double[] tuple) { + return rgb_to_hex(hsluv_to_rgb(tuple)); +} + +internal string hpluv_to_hex(double[] tuple) { + return rgb_to_hex(hpluv_to_rgb(tuple)); +} + +internal double[] hex_to_hsluv(string s) { + return rgb_to_hsluv(hex_to_tgb(s)); +} + +internal double[] hex_to_hpluv(string s) { + return rgb_to_hpluv(hex_to_tgb(s)); +} + +} \ No newline at end of file diff --git a/xmpp-vala/src/module/xep/0402_bookmarks2.vala b/xmpp-vala/src/module/xep/0402_bookmarks2.vala index 406f37f4..d1e53e6e 100644 --- a/xmpp-vala/src/module/xep/0402_bookmarks2.vala +++ b/xmpp-vala/src/module/xep/0402_bookmarks2.vala @@ -68,6 +68,11 @@ public class Module : BookmarksProvider, XmppStreamModule { } private void on_pupsub_item(XmppStream stream, Jid jid, string id, StanzaNode? node) { + if (!jid.equals(stream.get_flag(Bind.Flag.IDENTITY).my_jid.bare_jid)) { + warning("Received alleged bookmarks:1 item from %s, ignoring", jid.to_string()); + return; + } + Conference conference = parse_item_node(node, id); Flag? flag = stream.get_flag(Flag.IDENTITY); if (flag != null) { @@ -77,6 +82,11 @@ public class Module : BookmarksProvider, XmppStreamModule { } private void on_pupsub_retract(XmppStream stream, Jid jid, string id) { + if (!jid.equals(stream.get_flag(Bind.Flag.IDENTITY).my_jid.bare_jid)) { + warning("Received alleged bookmarks:1 retract from %s, ignoring", jid.to_string()); + return; + } + try { Jid jid_parsed = new Jid(id); Flag? flag = stream.get_flag(Flag.IDENTITY); diff --git a/xmpp-vala/tests/color.vala b/xmpp-vala/tests/color.vala new file mode 100644 index 00000000..ded67d53 --- /dev/null +++ b/xmpp-vala/tests/color.vala @@ -0,0 +1,50 @@ +using Xmpp.Xep; + +namespace Xmpp.Test { + +class ColorTest : Gee.TestCase { + + public ColorTest() { + base("color"); + + add_test("xep-vectors-angle", () => { text_xep_vectors_angle(); }); + add_test("xep-vectors-rgbf", () => { test_xep_vectors_rgbf(); }); + add_test("rgb-to-angle", () => { test_rgb_to_angle(); }); + } + + public void text_xep_vectors_angle() { + fail_if_not_eq_double(ConsistentColor.string_to_angle("Romeo"), 327.255249); + fail_if_not_eq_double(ConsistentColor.string_to_angle("juliet@capulet.lit"), 209.410400); + fail_if_not_eq_double(ConsistentColor.string_to_angle("😺"), 331.199341); + fail_if_not_eq_double(ConsistentColor.string_to_angle("council"), 359.994507); + fail_if_not_eq_double(ConsistentColor.string_to_angle("Board"), 171.430664); + } + + private bool fail_if_not_eq_rgbf(float[] left, float[] right) { + bool failed = false; + for (int i = 0; i < 3; i++) { + failed = fail_if_not_eq_float(left[i], right[i]) || failed; + } + return failed; + } + + public void test_xep_vectors_rgbf() { + fail_if_not_eq_rgbf(ConsistentColor.string_to_rgbf("Romeo"), {0.865f,0.000f,0.686f}); + fail_if_not_eq_rgbf(ConsistentColor.string_to_rgbf("juliet@capulet.lit"), {0.000f,0.515f,0.573f}); + fail_if_not_eq_rgbf(ConsistentColor.string_to_rgbf("😺"), {0.872f,0.000f,0.659f}); + fail_if_not_eq_rgbf(ConsistentColor.string_to_rgbf("council"), {0.918f,0.000f,0.394f}); + fail_if_not_eq_rgbf(ConsistentColor.string_to_rgbf("Board"), {0.000f,0.527f,0.457f}); + } + + public void test_rgb_to_angle() { + string[] colors = {"e57373", "f06292", "ba68c8", "9575cd", "7986cb", "64b5f6", "4fc3f7", "4dd0e1", "4db6ac", "81c784", "aed581", "dce775", "fff176", "ffd54f", "ffb74d", "ff8a65"}; + foreach(string hex_color in colors) { + uint8 r = (uint8) ((double) hex_color.substring(0, 2).to_long(null, 16)); + uint8 g = (uint8) ((double) hex_color.substring(2, 2).to_long(null, 16)); + uint8 b = (uint8) ((double) hex_color.substring(4, 2).to_long(null, 16)); + //print(@"$hex_color, $r, $g, $b, $(ConsistentColor.rgb_to_angle(r, g, b))\n"); + } + } +} + +} \ No newline at end of file diff --git a/xmpp-vala/tests/common.vala b/xmpp-vala/tests/common.vala index 47dbce0e..dc1c8e50 100644 --- a/xmpp-vala/tests/common.vala +++ b/xmpp-vala/tests/common.vala @@ -6,6 +6,7 @@ int main(string[] args) { TestSuite.get_root().add_suite(new Xmpp.Test.StanzaTest().get_suite()); TestSuite.get_root().add_suite(new Xmpp.Test.UtilTest().get_suite()); TestSuite.get_root().add_suite(new Xmpp.Test.JidTest().get_suite()); + TestSuite.get_root().add_suite(new Xmpp.Test.ColorTest().get_suite()); return GLib.Test.run(); } @@ -68,6 +69,22 @@ bool fail_if_not_eq_int(int left, int right, string? reason = null) { return fail_if_not(left == right, @"$(reason + ": " ?? "")$left != $right"); } +private float float_to_accuracy(float f, float accuracy) { + return (float) (Math.round(f * Math.pow(10, accuracy)) / Math.pow(10, accuracy)); +} + +private float double_to_accuracy(double f, float accuracy) { + return (float) (Math.round(f * Math.pow(10, accuracy)) / Math.pow(10, accuracy)); +} + +bool fail_if_not_eq_float(float left, float right, float accuracy = 3, string? reason = null) { + return fail_if_not(float_to_accuracy(left, accuracy) == float_to_accuracy(right, accuracy), @"$(reason + ": " ?? "")$left != $right"); +} + +bool fail_if_not_eq_double(double left, double right, float accuracy = 3, string? reason = null) { + return fail_if_not(double_to_accuracy(left, accuracy) == double_to_accuracy(right, accuracy), @"$(reason + ": " ?? "")$left != $right"); +} + bool fail_if_not_eq_str(string? left, string? right, string? reason = null) { bool nullcheck = (left == null || right == null) && (left != null && right != null); if (left == null) left = "(null)";