Compare commits

...

54 Commits

Author SHA1 Message Date
Marvin W
23301156ab Fix build with webrtc-audio-processing 2.x 2025-02-23 20:43:25 +01:00
Marvin W
6ec283ead2 Voice processor: add support for webrtc-audio-processing 1.x and 2.x 2025-02-23 20:43:25 +01:00
Marvin W
7b9c0d3c4c Fix vertical picture padding for certain aspect ratios 2025-02-23 20:43:25 +01:00
Marvin W
b58627bfd8 Fix various sizing issues 2025-02-23 20:43:25 +01:00
Marvin W
3da0149061 Fix compatibility with GTK 4.17+ 2025-02-23 20:43:25 +01:00
Marvin W
7b21383eb8 Search: Build preview string based on chars not bytes 2025-02-23 20:43:25 +01:00
Marvin W
6ef82b857d Work around pango bug
See https://gitlab.gnome.org/GNOME/pango/-/issues/798 and https://gitlab.gnome.org/GNOME/pango/-/issues/832
2025-02-23 20:43:25 +01:00
fiaxh
97054c45a3 XEP-0215: Remove timeout from previous connection 2025-02-23 20:43:25 +01:00
Alexandre Jousset
c5e32eca14 XEP-0215: management of the field expires
Add a field in `Xmpp.Xep.ExternalServiceDiscovery` to keep track
of the `expires` TURN service value and use it (divided by 2) to
restart periodically the external services discovery.
2025-02-23 20:43:25 +01:00
fiaxh
7285977b85 Ignore bad stun/turn IP addresses 2025-02-23 20:43:25 +01:00
fiaxh
585023c92e Omemo: Connect listener only once on account added 2025-02-23 20:43:25 +01:00
fiaxh
4113c324fe Fix shift+enter in chat input not scrolling down 2025-02-23 20:43:25 +01:00
Marvin W
85e99a2bc8 OMEMO: Do not show message for OMEMO messages without payload 2025-02-23 20:43:25 +01:00
Marvin W
17119ecc6d
Update appdata 2024-06-30 23:01:20 +02:00
Matthew Fennell
b1e0244e9a
Allow self-signed .onion file transfer certs (#1149)
Most Certificate Authorities don't support issuing X.509 certificates for onion
sites. However, it can still be useful to provide a certificate over Tor in
some circumstances, for instance to tie your alphanumeric Tor address to your
site's main identity.

Therefore, many Tor services provide self-signed certificates. This is OK,
since the onion service itself guarantees that you are connecting to the entity
you think you are.

Dino already allows self-signed certs when communicating over Tor (see
81a5505). However, the same exception does not exist yet for HTTP uploads and
downloads - causing these to fail over Tor.

Therefore, in this commit, we add the same exception for uploads/downloads, by
passing the host of the upload/download urls to the already existing invalid
certificate connection handler.

Note that this handler only allows certificates with type
TlsCertificateFlags.UNKNOWN_CA. This means the certificate of your server must
also include the onion http upload and download URLs in its certificate -
otherwise, the file transfer will fail with TlsCertificateFlags.BAD_IDENTITY.
2024-06-30 23:01:06 +02:00
Matthew Fennell
2bd0fc30fe
Self-ping with server-given roomnick (#1594)
* Self-ping with server-given roomnick (#1467)

XEP-0045 describes how a server can modify a user's roomnick when joining a
room. To do this, it assigns status code 210, and sets the "from" attribute to
a JID with thew newly assigned nickname when sending the user's presence back
to the client.

This is used in IRC gateways such as biboumi. Since you can only have one nick
per IRC server, if you try and join multiple IRC-bridged MUCs on the same IRC
server, it will modify your nick to match the existing one you have on that
server.

Currently, when Dino performs a self-ping after joining a room, it uses the
nick that the client first requested, instead of the nick returned from the
server. This can lead to incorrect self-pings being sent out (until the client
is restarted, and populates mucs_todo correctly). This happens because it adds
the requested Jid to mucs_todo, before getting the given nick from the server.

Therefore, this commit adds the jid that has been given from the server
instead. If there is any error in requesting to join the MUC, it adds the
requested nick, to match existing behaviour and allow the request to be retried
via future self-pings.

fixes #1467

* Minor coding style changes

---------

Co-authored-by: fiaxh <git@lightrise.org>
2024-06-30 22:59:49 +02:00
fiaxh
39474d02e4
Join Channel dialog: Fix displaying of bookmark details 2024-06-30 22:59:31 +02:00
Klemens Nanni
24876bee35
Always export symbols to fix startup on BSDs
```
$ dino
(dino:38515): Gtk-ERROR **: 15:38:38.538: failed to add UI from resource /im/dino/Dino/unified_main_content.ui: .:26:1 Invalid object type 'DinoUiConversationSelector'
Trace/BPT trap (core dumped)
```

This works on Linux because CMake itself links with `-rdynamic` by default
as per its `Modules/Platform/Linux-*.cmake`.

OpenBSD carries this as local patch, FreeBSD links with `--export-dynamics`.
Just linking with `-rdynamic` also fixes it on OpenBSD, as expected.

https://cmake.org/cmake/help/latest/prop_tgt/ENABLE_EXPORTS.html

Fix #438.
2024-05-15 14:57:52 +02:00
Marvin W
235efcdab9
AppData/DOAP: Use sentence case for summary.
The suggestion is to not use title case which we did before.
2024-05-09 17:23:56 +02:00
Marvin W
08c18f884f
AppData: Add brand color 2024-05-09 17:23:17 +02:00
Rico Tzschichholz
a90edd0538
Annotating with array_null_terminated doesn't imply a missing array_length
Taken from 6b8a3e4faa
2024-05-09 17:20:06 +02:00
eerielili
f4b7e661cf
Fix message stanza with body changing MUC subject (#1569)
- fixes https://github.com/dino/dino/issues/1542
            - more consistent with
              https://xmpp.org/extensions/xep-0045.html#enter-subject:
                    "Note: In accordance with the core definition of XML stanzas,
                    any message can contain a <subject/> element; only a message that
                    contains a <subject/> but no <body/> element shall be considered a
                    subject change for MUC purposes."
2024-05-09 17:09:22 +02:00
eerielili
f39032bb94
Fix poor contrast of highlight in search results with dark theme (#1557)
- fixes #1308
2024-05-09 17:09:11 +02:00
eerielili
7dc1657740
Fix crash on removing conference not in roster (#1516) 2024-05-09 17:09:03 +02:00
eerielili
2d555c64ab
Fix http upload for servers without file size limit (#1512)
* Fix for ejabberd XMPP server 'infinity' http upload file size announce

	- fixes https://github.com/dino/dino/issues/1222

* Update 0363_http_file_upload.vala
2024-05-09 17:08:52 +02:00
eerielili
b54582b8dd
Start conversation if closed when receiving an audio or video call (#1485)
* Start conversation if closed when receiving an audio or video call

* Fix starting conversation on new calls, move setting conversation.last_active

---------

Co-authored-by: fiaxh <git@lightrise.org>
2024-05-09 17:08:40 +02:00
fiaxh
ed7bad4a73
Fix crash due to gpg binding issue 2024-05-09 17:08:05 +02:00
fiaxh
cee0bfb22e
Fix implicit-function-declaration compiler warnings 2024-05-09 17:05:44 +02:00
fiaxh
b7b37e770e
Fix subscription notification clearing 2024-05-09 17:05:18 +02:00
mesonium
67cb8b4af0
fix: Add x node to MUC PM stanza (#1462)
Add <x/> tag in MUC-PMs to support better Carbon delivery in
compliance with XEP-0045 v1.28 and above.

Fixes #1306
2024-05-09 17:05:12 +02:00
Kim Alvefur
f4e761cec4
Fix showing the kick option to owners
Missing case in the switch defaulted to returning false for Owners, thus
preventing they with the most privileges from using those privileges.
2024-05-09 17:05:08 +02:00
Marvin W
df0013e3f4
Fix potential crash in video calls 2023-07-09 18:24:34 +02:00
Marvin W
0521f50146
Fix certificate start time
I doubt anyone ever looked at it, but it shouldn't be 1 day in the future ;)
2023-07-09 18:24:34 +02:00
Marvin W
92b7d97339
Do not send DTLS datagrams to RTP even after handshake
Also post debug message in case we drop datagrams
2023-07-09 18:24:34 +02:00
Stephen Paul Weber
7c048ecaef
Ignore non-DTLS data before handshake is complete
https://datatracker.ietf.org/doc/html/rfc9147#name-demul
https://datatracker.ietf.org/doc/html/rfc5764#section-5.1.2

If data is received before handshake is complete, discard it rather than
forwarding it blindly to GnuTLS which can get confused.
2023-07-09 18:24:34 +02:00
Robert Mader
da383a2fe9
data: Set X-Purism-FormFactor in .desktop file
So the app is detected as mobile-friendly on Phosh.
2023-07-09 18:24:15 +02:00
Marvin W
3a925c4c00
Fix reactions being made to the wrong message
fixes #1426
2023-07-09 18:23:43 +02:00
fiaxh
0ed0495e0a Fix chat input for IME
fixes #1419

Co-authored-by: Marvin W <git@larma.de>
2023-05-14 14:39:20 +02:00
fiaxh
569d4141cf Fix chat input status having a fixed width requirement
fixes #1439
2023-05-14 14:39:20 +02:00
fiaxh
f44ded88c8 Fix character counting for fallbacks
fixes #1420
2023-05-14 14:39:20 +02:00
Karim Malhas
a74b894147 Focus ChatInput textbox after selecting emoji
After selecting an emoji, the emoji is inserted
into the textbox, but focus remains on the emoji_button.

This causes the EmojiChooser to be opened again if a user
hits the Enter key directly, but text is inserted into the textbox
if they continue to type.

This commit just explicitely focuses on the textbox after
an emoji has been selected.
2023-05-14 14:39:20 +02:00
fiaxh
f011adac37 Fix crash on NS_URI call when own server has no MAM; drop broken mam:1 "support"
fixes #1405
2023-05-14 14:39:20 +02:00
Marvin W
58379acc9f Fix empty alias being handled different than none 2023-05-14 14:39:20 +02:00
fiaxh
2fb8d29b34 Fix call window styling 2023-05-14 14:39:20 +02:00
fiaxh
7a27634732 Fix call window controlls hiding 2023-05-14 14:39:20 +02:00
Marvin W
d155ec15d2 Fix video for cameras with rotated image 2023-05-14 14:39:20 +02:00
Marvin W
baf96d9d9f
Check sender of bookmark:1 updates 2023-03-23 12:06:36 -06:00
Marvin W
179c766d19
Bind soup session lifetime to File provider/sender lifetime
Required since libsoup 3.4. Fixes #1395
2023-03-22 13:22:23 -06:00
Bohdan Horbeshko
004824040d
Fix a crash if a message subnode is not found in a carbon
Fixes #1392
2023-03-22 09:59:55 -06:00
Michael Vetter
b6f9b54d76
Remove gspell
7e7dcedaf ported from GTK3 to GTK4.
It also removed gspell from main/CMakeLists.txt.

I assume that gspell is not needed anymore and we can thus remove the
requirement from the CI and the cmake file as well.
2023-03-22 09:59:55 -06:00
Sebastian Krzyszkowiak
1738bf8dc8
data: Set StartupNotify to true in .desktop file
GTK handles startup notifications, so advertise it in desktop
file. This allows splash screens and other startup indications
in DEs to work.
2023-03-22 09:59:54 -06:00
Marvin W
481a68fd89
Improve database performance while reconnecting and syncing
Also move some tasks to low priority idle queue so they won't block UI updates
2023-03-22 09:59:54 -06:00
Marvin W
89b9110fcb
Improve history sync
- Ensure we fully fetch desired history if possible (previously, duplicates
  from offline message queue could hinder MAM sync)
- Early drop illegal MAM messages so they don't pile up in the pending queue
  waiting for their query to end (which it never will if they were not
  requested in first place).

Fixes #1386
2023-03-22 09:59:54 -06:00
Marvin W
acf9c69470
Fix C binding for gst_video_frame_get_data
Fixes #1267
2023-03-22 09:59:54 -06:00
112 changed files with 1020 additions and 390 deletions

View File

@ -7,7 +7,7 @@ jobs:
- uses: actions/checkout@v2
- 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

View File

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

View File

@ -1,10 +1,38 @@
include(PkgConfigWithFallback)
find_pkg_config_with_fallback(WebRTCAudioProcessing
PKG_CONFIG_NAME webrtc-audio-processing-1
LIB_NAMES webrtc_audio_processing-1
INCLUDE_NAMES modules/audio_processing/include/audio_processing.h
INCLUDE_DIR_SUFFIXES webrtc-audio-processing-1 webrtc_audio_processing-1
)
if(WebRTCAudioProcessing_FOUND AND NOT WebRTCAudioProcessing_VERSION)
set(WebRTCAudioProcessing_VERSION "1.0")
endif()
if(NOT WebRTCAudioProcessing_FOUND)
find_pkg_config_with_fallback(WebRTCAudioProcessing
PKG_CONFIG_NAME webrtc-audio-processing
LIB_NAMES webrtc_audio_processing
INCLUDE_NAMES webrtc/modules/audio_processing/include/audio_processing.h
INCLUDE_DIR_SUFFIXES webrtc-audio-processing webrtc_audio_processing
)
)
if(WebRTCAudioProcessing_FOUND AND NOT WebRTCAudioProcessing_VERSION)
set(WebRTCAudioProcessing_VERSION "0.2")
endif()
endif(NOT WebRTCAudioProcessing_FOUND)
if(NOT WebRTCAudioProcessing_FOUND)
find_pkg_config_with_fallback(WebRTCAudioProcessing
PKG_CONFIG_NAME webrtc-audio-processing-2
LIB_NAMES webrtc_audio_processing-2
INCLUDE_NAMES modules/audio_processing/include/audio_processing.h
INCLUDE_DIR_SUFFIXES webrtc-audio-processing-2 webrtc_audio_processing-2
)
if(WebRTCAudioProcessing_FOUND AND NOT WebRTCAudioProcessing_VERSION)
set(WebRTCAudioProcessing_VERSION "2.0")
endif()
endif(NOT WebRTCAudioProcessing_FOUND)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(WebRTCAudioProcessing

View File

@ -15,9 +15,15 @@ function(find_pkg_config_with_fallback name)
# Try to find real file name of libraries
foreach(lib ${${name}_PKG_CONFIG_LIBRARIES})
if (${lib} STREQUAL "atomic")
set(${name}_${lib}_LIBRARY ${lib})
mark_as_advanced(${name}_${lib}_LIBRARY)
continue()
endif()
find_library(${name}_${lib}_LIBRARY ${lib} HINTS ${${name}_PKG_CONFIG_LIBRARY_DIRS})
mark_as_advanced(${name}_${lib}_LIBRARY)
if(NOT ${name}_${lib}_LIBRARY)
message(STATUS "Required library for package " ${name} " not found: " ${lib})
unset(${name}_FOUND)
endif(NOT ${name}_${lib}_LIBRARY)
endforeach(lib)

View File

@ -3,7 +3,7 @@
<Project>
<name>Dino</name>
<short-name>dino</short-name>
<shortdesc xml:lang="en">Modern XMPP Chat Client</shortdesc>
<shortdesc xml:lang="en">Modern XMPP chat client</shortdesc>
<shortdesc xml:lang="zh-TW">現代化的 XMPP 用戶端聊天軟件</shortdesc>
<shortdesc xml:lang="zh-CN">现代 XMPP 聊天客户端</shortdesc>
<shortdesc xml:lang="tr">Modern XMPP Sohbet İstemcisi</shortdesc>

View File

@ -9,7 +9,7 @@
<name>Dino</name>
<short-name>dino</short-name>
<shortdesc xml:lang="en">Modern XMPP Chat Client</shortdesc>
<shortdesc xml:lang="en">Modern XMPP chat client</shortdesc>
<description xml:lang="en">
Dino is a modern open-source chat client for the desktop. It focuses on providing a clean and reliable Jabber/XMPP experience while having your privacy in mind.
It supports end-to-end encryption with OMEMO and OpenPGP and allows configuring privacy-related features such as read receipts and typing notifications.

View File

@ -40,12 +40,12 @@ public interface Application : GLib.Application {
PresenceManager.start(stream_interactor);
CounterpartInteractionManager.start(stream_interactor);
BlockingManager.start(stream_interactor);
Calls.start(stream_interactor, db);
ConversationManager.start(stream_interactor, db);
MucManager.start(stream_interactor);
AvatarManager.start(stream_interactor, db);
RosterManager.start(stream_interactor, db);
FileManager.start(stream_interactor, db);
Calls.start(stream_interactor, db);
CallStore.start(stream_interactor, db);
ContentItemStore.start(stream_interactor, db);
ChatInteraction.start(stream_interactor);

View File

@ -202,6 +202,7 @@ public class Message : Object {
}
public static uint hash_func(Message message) {
if (message.body == null) return 0;
return message.body.hash();
}

View File

@ -61,8 +61,6 @@ namespace Dino {
call_state.initiate_groupchat_call.begin(conversation.counterpart);
}
conversation.last_active = call.time;
call_outgoing(call, call_state, conversation);
return call_state;
@ -221,7 +219,6 @@ namespace Dino {
Conversation conversation = stream_interactor.get_module(ConversationManager.IDENTITY).create_conversation(call.counterpart.bare_jid, account, Conversation.Type.CHAT);
stream_interactor.get_module(CallStore.IDENTITY).add_call(call, conversation);
conversation.last_active = call.time;
var call_state = new CallState(call, stream_interactor);
connect_call_state_signals(call_state);
@ -294,7 +291,6 @@ namespace Dino {
Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(inviter_jid.bare_jid, account);
if (conversation == null) return null;
stream_interactor.get_module(CallStore.IDENTITY).add_call(call, conversation);
conversation.last_active = call.time;
CallState call_state = new CallState(call, stream_interactor);
connect_call_state_signals(call_state);
@ -465,7 +461,6 @@ namespace Dino {
Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).approx_conversation_for_stanza(from_jid, to_jid, account, message_stanza.type_);
if (conversation == null) return;
conversation.last_active = call_state.call.time;
if (call_state.call.direction == Call.DIRECTION_INCOMING) {
call_incoming(call_state.call, call_state, conversation, video_requested, multiparty);

View File

@ -29,6 +29,8 @@ public class ConversationManager : StreamInteractionModule, Object {
stream_interactor.account_removed.connect(on_account_removed);
stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(new MessageListener(stream_interactor));
stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent.connect(handle_sent_message);
stream_interactor.get_module(Calls.IDENTITY).call_incoming.connect(handle_new_call);
stream_interactor.get_module(Calls.IDENTITY).call_outgoing.connect(handle_new_call);
}
public Conversation create_conversation(Jid jid, Account account, Conversation.Type? type = null) {
@ -194,6 +196,11 @@ public class ConversationManager : StreamInteractionModule, Object {
}
}
private void handle_new_call(Call call, CallState state, Conversation conversation) {
conversation.last_active = call.time;
start_conversation(conversation);
}
private void add_conversation(Conversation conversation) {
if (!conversations[conversation.account].has_key(conversation.counterpart)) {
conversations[conversation.account][conversation.counterpart] = new ArrayList<Conversation>(Conversation.equals_func);

View File

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

View File

@ -90,11 +90,9 @@ public class Dino.HistorySync {
if (!is_muc_mam && !from_our_server) return;
// Get the server time of the message and store it in `mam_times`
Xmpp.MessageArchiveManagement.Flag? mam_flag = stream != null ? stream.get_flag(Xmpp.MessageArchiveManagement.Flag.IDENTITY) : null;
if (mam_flag == null) return;
string? id = message.stanza.get_deep_attribute(mam_flag.ns_ver + ":result", "id");
string? id = message.stanza.get_deep_attribute(Xmpp.MessageArchiveManagement.NS_URI + ":result", "id");
if (id == null) return;
StanzaNode? delay_node = message.stanza.get_deep_subnode(mam_flag.ns_ver + ":result", StanzaForwarding.NS_URI + ":forwarded", DelayedDelivery.NS_URI + ":delay");
StanzaNode? delay_node = message.stanza.get_deep_subnode(Xmpp.MessageArchiveManagement.NS_URI + ":result", StanzaForwarding.NS_URI + ":forwarded", DelayedDelivery.NS_URI + ":delay");
if (delay_node == null) {
warning("MAM result did not contain delayed time %s", message.stanza.to_string());
return;
@ -104,7 +102,7 @@ public class Dino.HistorySync {
mam_times[account][id] = time;
// Check if this is the target message
string? query_id = message.stanza.get_deep_attribute(mam_flag.ns_ver + ":result", mam_flag.ns_ver + ":queryid");
string? query_id = message.stanza.get_deep_attribute(Xmpp.MessageArchiveManagement.NS_URI + ":result", Xmpp.MessageArchiveManagement.NS_URI + ":queryid");
if (query_id != null && id == catchup_until_id[account]) {
debug("[%s] Hitted range (id) %s", account.bare_jid.to_string(), id);
hitted_range[query_id] = -2;
@ -163,7 +161,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 +212,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 +268,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 +278,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 +326,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 +353,6 @@ public class Dino.HistorySync {
MorePagesAvailable,
TargetReached,
NoMoreMessages,
Duplicate,
Error,
Cancelled
}
@ -399,23 +394,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 +420,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 +430,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) {

View File

@ -170,19 +170,21 @@ public class MessageProcessor : StreamInteractionModule, Object {
XmppStream? stream = stream_interactor.get_stream(account);
Xmpp.MessageArchiveManagement.MessageFlag? mam_message_flag = Xmpp.MessageArchiveManagement.MessageFlag.get_flag(message);
Xmpp.MessageArchiveManagement.Flag? mam_flag = stream != null ? stream.get_flag(Xmpp.MessageArchiveManagement.Flag.IDENTITY) : null;
EntityInfo entity_info = stream_interactor.get_module(EntityInfo.IDENTITY);
if (mam_message_flag != null && mam_flag != null && mam_flag.ns_ver == Xmpp.MessageArchiveManagement.NS_URI_2 && mam_message_flag.mam_id != null) {
if (mam_message_flag != null && mam_message_flag.mam_id != null) {
bool server_does_mam = entity_info.has_feature_cached(account, account.bare_jid, Xmpp.MessageArchiveManagement.NS_URI);
if (server_does_mam) {
new_message.server_id = mam_message_flag.mam_id;
}
} else if (message.type_ == Xmpp.MessageStanza.TYPE_GROUPCHAT) {
bool server_supports_sid = (yield entity_info.has_feature(account, new_message.counterpart.bare_jid, Xep.UniqueStableStanzaIDs.NS_URI)) ||
(yield entity_info.has_feature(account, new_message.counterpart.bare_jid, Xmpp.MessageArchiveManagement.NS_URI_2));
(yield entity_info.has_feature(account, new_message.counterpart.bare_jid, Xmpp.MessageArchiveManagement.NS_URI));
if (server_supports_sid) {
new_message.server_id = Xep.UniqueStableStanzaIDs.get_stanza_id(message, new_message.counterpart.bare_jid);
}
} else if (message.type_ == Xmpp.MessageStanza.TYPE_CHAT) {
bool server_supports_sid = (yield entity_info.has_feature(account, account.bare_jid, Xep.UniqueStableStanzaIDs.NS_URI)) ||
(yield entity_info.has_feature(account, account.bare_jid, Xmpp.MessageArchiveManagement.NS_URI_2));
(yield entity_info.has_feature(account, account.bare_jid, Xmpp.MessageArchiveManagement.NS_URI));
if (server_supports_sid) {
new_message.server_id = Xep.UniqueStableStanzaIDs.get_stanza_id(message, account.bare_jid);
}
@ -378,7 +380,7 @@ public class MessageProcessor : StreamInteractionModule, Object {
public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
bool is_mam_message = Xmpp.MessageArchiveManagement.MessageFlag.get_flag(stanza) != null;
XmppStream? stream = stream_interactor.get_stream(conversation.account);
Xmpp.MessageArchiveManagement.Flag? mam_flag = stream != null ? stream.get_flag(Xmpp.MessageArchiveManagement.Flag.IDENTITY) : null;
Xmpp.MessageArchiveManagement.Flag mam_flag = Xmpp.MessageArchiveManagement.Flag.get_flag(stream);
if (is_mam_message || (mam_flag != null && mam_flag.cought_up == true)) {
conversation.account.mam_earliest_synced = message.local_time;
}
@ -494,8 +496,7 @@ public class MessageProcessor : StreamInteractionModule, Object {
string fallback = FallbackBody.get_quoted_fallback_body(content_item);
long fallback_length = fallback.length;
var fallback_location = new Xep.FallbackIndication.FallbackLocation(0, (int)fallback_length);
var fallback_location = new Xep.FallbackIndication.FallbackLocation(0, (int)fallback.char_count());
Xep.FallbackIndication.set_fallback(new_stanza, new Xep.FallbackIndication.Fallback(Xep.Replies.NS_URI, new Xep.FallbackIndication.FallbackLocation[] { fallback_location }));
return fallback;

View File

@ -54,6 +54,7 @@ public class MucManager : StreamInteractionModule, Object {
}
return true;
});
stream_interactor.get_module(MessageProcessor.IDENTITY).build_message_stanza.connect(on_build_message_stanza);
}
// already_autojoin: Without this flag we'd be retrieving bookmarks (to check for autojoin) from the sender on every join
@ -72,7 +73,7 @@ public class MucManager : StreamInteractionModule, Object {
bool receive_history = true;
EntityInfo entity_info = stream_interactor.get_module(EntityInfo.IDENTITY);
bool can_do_mam = yield entity_info.has_feature(account, jid, Xmpp.MessageArchiveManagement.NS_URI_2);
bool can_do_mam = yield entity_info.has_feature(account, jid, Xmpp.MessageArchiveManagement.NS_URI);
if (can_do_mam) {
receive_history = false;
history_since = null;
@ -83,11 +84,6 @@ public class MucManager : StreamInteractionModule, Object {
}
mucs_joining[account].add(jid);
if (!mucs_todo.has_key(account)) {
mucs_todo[account] = new HashSet<Jid>(Jid.hash_bare_func, Jid.equals_bare_func);
}
mucs_todo[account].add(jid.with_resource(nick_));
Muc.JoinResult? res = yield stream.get_module(Xep.Muc.Module.IDENTITY).enter(stream, jid.bare_jid, nick_, password, history_since, receive_history, null);
mucs_joining[account].remove(jid);
@ -126,6 +122,11 @@ public class MucManager : StreamInteractionModule, Object {
enter_errors[jid] = res.muc_error;
}
if (!mucs_todo.has_key(account)) {
mucs_todo[account] = new HashSet<Jid>(Jid.hash_bare_func, Jid.equals_bare_func);
}
mucs_todo[account].add(jid.with_resource(res.nick ?? nick_));
return res;
}
@ -651,6 +652,12 @@ public class MucManager : StreamInteractionModule, Object {
conference_removed(account, jid);
}
private void on_build_message_stanza(Entities.Message message, Xmpp.MessageStanza message_stanza, Conversation conversation) {
if (conversation.type_ == Conversation.Type.GROUPCHAT_PM) {
Xmpp.Xep.Muc.add_muc_pm_message_stanza_x_node(message_stanza);
}
}
private void self_ping(Account account) {
XmppStream? stream = stream_interactor.get_stream(account);
if (stream == null) return;

View File

@ -64,7 +64,7 @@ public class Dino.Reactions : StreamInteractionModule, Object {
// The MUC server needs to 1) support stable stanza ids 2) either support occupant ids or be a private room (where we know real jids)
var entity_info = stream_interactor.get_module(EntityInfo.IDENTITY);
bool server_supports_sid = (entity_info.has_feature_cached(conversation.account, conversation.counterpart.bare_jid, Xep.UniqueStableStanzaIDs.NS_URI)) ||
(entity_info.has_feature_cached(conversation.account, conversation.counterpart.bare_jid, Xmpp.MessageArchiveManagement.NS_URI_2));
(entity_info.has_feature_cached(conversation.account, conversation.counterpart.bare_jid, Xmpp.MessageArchiveManagement.NS_URI));
if (!server_supports_sid) return false;
bool? supports_occupant_ids = entity_info.has_feature_cached(conversation.account, conversation.counterpart, Xep.OccupantIds.NS_URI);

View File

@ -105,7 +105,8 @@ namespace Dino {
string body = message.body;
foreach (var fallback in message.get_fallbacks()) {
if (fallback.ns_uri == Xep.Replies.NS_URI && message.quoted_item_id > 0) {
body = body[0:fallback.locations[0].from_char] + body[fallback.locations[0].to_char:body.length];
body = body[0:body.index_of_nth_char(fallback.locations[0].from_char)] +
body[body.index_of_nth_char(fallback.locations[0].to_char):body.length];
}
}
return body;

View File

@ -34,6 +34,7 @@ namespace Dino {
if (self_word != null && (account.alias == null || account.alias.length == 0)) {
return self_word;
}
if (account.alias != null && account.alias.length == 0) return null;
return account.alias;
}
Roster.Item roster_item = stream_interactor.get_module(RosterManager.IDENTITY).get_roster_item(account, jid);

View File

@ -232,6 +232,7 @@ add_executable(dino ${MAIN_VALA_C} ${MAIN_GRESOURCES_TARGET})
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)

View File

@ -7,8 +7,8 @@
</style>
<child>
<object class="DinoUiSizingBin">
<property name="target-width">350</property>
<property name="max-width">350</property>
<property name="target-width">400</property>
<property name="max-width">400</property>
<property name="hexpand">True</property>
<child>
<object class="GtkBox">

View File

@ -85,6 +85,7 @@
<property name="margin_bottom">3</property>
<property name="margin_start">14</property>
<property name="margin_end">14</property>
<property name="wrap">True</property>
<attributes>
<attribute name="scale" value="0.8"></attribute>
</attributes>

View File

@ -14,8 +14,6 @@
<property name="valign">end</property>
<child>
<object class="GtkOverlay">
<child>
<object class="GtkBox" id="main_event_box">
<child>
<object class="DinoUiSizeRequestBox" id="main">
<property name="margin-bottom">15</property>
@ -23,8 +21,6 @@
<property name="size-request-mode">height-for-width</property>
</object>
</child>
</object>
</child>
<child type="overlay">
<object class="GtkBox" id="message_menu_box">
<property name="margin-end">10</property>

View File

@ -8,8 +8,8 @@
</style>
<child>
<object class="DinoUiSizingBin">
<property name="target-width">500</property>
<property name="max-width">500</property>
<property name="target-width">400</property>
<property name="max-width">400</property>
<property name="hexpand">True</property>
<child>
<object class="GtkBox">

View File

@ -1,11 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop">
<id>im.dino.Dino</id>
<launchable type="desktop-id">im.dino.Dino.desktop</launchable>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0+</project_license>
<name>Dino</name>
<summary>Modern XMPP Chat Client</summary>
<summary>Modern XMPP chat client</summary>
<summary xml:lang="zh_TW">現代化的 XMPP 用戶端聊天軟件</summary>
<summary xml:lang="zh_CN">现代 XMPP 聊天客户端</summary>
<summary xml:lang="tr">Modern XMPP Sohbet İstemcisi</summary>
@ -39,6 +37,8 @@
<summary xml:lang="cs">Moderní XMPP klient</summary>
<summary xml:lang="ca">Client de xat XMPP modern</summary>
<summary xml:lang="ar">تطبيق حديث للدردشة عبر XMPP</summary>
<icon type="stock">im.dino.Dino</icon>
<icon type="remote" width="128" height="128">https://dino.im/img/appdata/icon-dino-0.4-128x128.png</icon>
<description>
<p>Dino is a modern open-source chat client for the desktop. It focuses on providing a clean and reliable Jabber/XMPP experience while having your privacy in mind.</p>
<p xml:lang="zh_TW">Dino 是一個爲桌面打造的現代化開放原始碼用戶端聊天軟件。它致力於提供一份簡洁而可靠的 Jabber/XMPP 體驗,同時亦尊重您的私隱。</p>
@ -146,24 +146,22 @@
<p xml:lang="ca">Dino recupera l'historial del servidor i sincronitza els missatges amb altres dispositius.</p>
<p xml:lang="ar">يقوم Dino بجلب السِجلّ مِن السيرفر ثم يُزامِن الرسائل مع الأجهزة الأخرى.</p>
</description>
<screenshots>
<screenshot type="default">
<image>https://dino.im/img/appdata/2022-02_screenshot-main.png</image>
</screenshot>
<screenshot>
<image>https://dino.im/img/appdata/2022-02_screenshot-call.png</image>
</screenshot>
<screenshot>
<image>https://dino.im/img/appdata/start_chat.png</image>
</screenshot>
</screenshots>
<translation type="gettext">dino</translation>
<developer_name>Dino Development Team</developer_name>
<categories>
<category>Network</category>
<category>InstantMessaging</category>
<category>Chat</category>
<category>GTK</category>
</categories>
<keywords>
<keyword translate="no">Jabber</keyword>
<keyword translate="no">XMPP</keyword>
</keywords>
<url type="homepage">https://dino.im</url>
<url type="bugtracker">https://github.com/dino/dino/issues</url>
<url type="donation">https://dino.im/#donate</url>
<url type="vcs-browser">https://github.com/dino/dino</url>
<url type="translate">https://hosted.weblate.org/projects/dino/</url>
<update_contact>appstream@dino.im</update_contact>
<launchable type="desktop-id">im.dino.Dino.desktop</launchable>
<releases>
<release date="2023-02-07" version="0.4">
<description>
@ -186,8 +184,50 @@
</description>
</release>
</releases>
<provides>
<mediatype>x-scheme-handler/xmpp</mediatype>
</provides>
<recommends>
<internet>always</internet>
</recommends>
<supports>
<control>pointing</control>
<control>keyboard</control>
<control>touch</control>
</supports>
<project_license>GPL-3.0+</project_license>
<developer id="im.dino">
<name>Dino Development Team</name>
<url>https://dino.im/</url>
</developer>
<!-- Deprecated: -->
<developer_name>Dino Development Team</developer_name>
<screenshots>
<screenshot type="default">
<image type="source" width="2244" height="1644" scale="2">https://dino.im/img/appdata/screenshot-dino-0.4-main-2244x1644@2.png</image>
<caption>Main screen</caption>
</screenshot>
<screenshot>
<image type="source" width="1441" height="929" scale="2">https://dino.im/img/appdata/screenshot-dino-0.4-call-1441x929@2.png</image>
<caption>Video call screen</caption>
</screenshot>
<screenshot>
<image type="source" width="2244" height="1644" scale="2">https://dino.im/img/appdata/screenshot-dino-0.4-search-2244x1644@2.png</image>
<caption>Main screen featuring search overlay</caption>
</screenshot>
<screenshot>
<image type="source" width="964" height="1552" scale="2">https://dino.im/img/appdata/screenshot-dino-0.4-mobile-964x1552@2.png</image>
<caption>Main screen in mobile size window</caption>
</screenshot>
</screenshots>
<translation type="gettext">dino</translation>
<content_rating type="oars-1.1">
<content_attribute id="social-chat">intense</content_attribute>
<content_attribute id="social-audio">intense</content_attribute>
</content_rating>
<update_contact>appstream@dino.im</update_contact>
<branding>
<color type="primary" scheme_preference="light">#B2DFDB</color>
<color type="primary" scheme_preference="dark">#004D40</color>
</branding>
</component>

View File

@ -1,34 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop">
<id>im.dino.Dino</id>
<launchable type="desktop-id">im.dino.Dino.desktop</launchable>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0+</project_license>
<name>Dino</name>
<summary>Modern XMPP Chat Client</summary>
<summary>Modern XMPP chat client</summary>
<icon type="stock">im.dino.Dino</icon>
<icon type="remote" width="128" height="128">https://dino.im/img/appdata/icon-dino-0.4-128x128.png</icon>
<description>
<p>Dino is a modern open-source chat client for the desktop. It focuses on providing a clean and reliable Jabber/XMPP experience while having your privacy in mind.</p>
<p>It supports end-to-end encryption with OMEMO and OpenPGP and allows configuring privacy-related features such as read receipts and typing notifications.</p>
<p>Dino fetches history from the server and synchronizes messages with other devices.</p>
</description>
<screenshots>
<screenshot type="default">
<image>https://dino.im/img/appdata/2022-02_screenshot-main.png</image>
</screenshot>
<screenshot>
<image>https://dino.im/img/appdata/2022-02_screenshot-call.png</image>
</screenshot>
<screenshot>
<image>https://dino.im/img/appdata/start_chat.png</image>
</screenshot>
</screenshots>
<translation type="gettext">dino</translation>
<developer_name>Dino Development Team</developer_name>
<categories>
<category>Network</category>
<category>InstantMessaging</category>
<category>Chat</category>
<category>GTK</category>
</categories>
<keywords>
<keyword translate="no">Jabber</keyword>
<keyword translate="no">XMPP</keyword>
</keywords>
<url type="homepage">https://dino.im</url>
<url type="bugtracker">https://github.com/dino/dino/issues</url>
<url type="donation">https://dino.im/#donate</url>
<url type="vcs-browser">https://github.com/dino/dino</url>
<url type="translate">https://hosted.weblate.org/projects/dino/</url>
<update_contact>appstream@dino.im</update_contact>
<launchable type="desktop-id">im.dino.Dino.desktop</launchable>
<releases>
<release date="2023-02-07" version="0.4">
<description>
@ -51,8 +49,50 @@
</description>
</release>
</releases>
<provides>
<mediatype>x-scheme-handler/xmpp</mediatype>
</provides>
<recommends>
<internet>always</internet>
</recommends>
<supports>
<control>pointing</control>
<control>keyboard</control>
<control>touch</control>
</supports>
<project_license>GPL-3.0+</project_license>
<developer id="im.dino">
<name>Dino Development Team</name>
<url>https://dino.im/</url>
</developer>
<!-- Deprecated: -->
<developer_name>Dino Development Team</developer_name>
<screenshots>
<screenshot type="default">
<image type="source" width="2244" height="1644" scale="2">https://dino.im/img/appdata/screenshot-dino-0.4-main-2244x1644@2.png</image>
<caption>Main screen</caption>
</screenshot>
<screenshot>
<image type="source" width="1441" height="929" scale="2">https://dino.im/img/appdata/screenshot-dino-0.4-call-1441x929@2.png</image>
<caption>Video call screen</caption>
</screenshot>
<screenshot>
<image type="source" width="2244" height="1644" scale="2">https://dino.im/img/appdata/screenshot-dino-0.4-search-2244x1644@2.png</image>
<caption>Main screen featuring search overlay</caption>
</screenshot>
<screenshot>
<image type="source" width="964" height="1552" scale="2">https://dino.im/img/appdata/screenshot-dino-0.4-mobile-964x1552@2.png</image>
<caption>Main screen in mobile size window</caption>
</screenshot>
</screenshots>
<translation type="gettext">dino</translation>
<content_rating type="oars-1.1">
<content_attribute id="social-chat">intense</content_attribute>
<content_attribute id="social-audio">intense</content_attribute>
</content_rating>
<update_contact>appstream@dino.im</update_contact>
<branding>
<color type="primary" scheme_preference="light">#B2DFDB</color>
<color type="primary" scheme_preference="dark">#004D40</color>
</branding>
</component>

View File

@ -5,9 +5,10 @@ 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;
X-GNOME-UsesNotifications=true
MimeType=x-scheme-handler/xmpp;
X-Purism-FormFactor=Workstation;Mobile;

View File

@ -392,17 +392,13 @@ box.dino-input-error .chat-input-status.input-status-highlight-once {
text-shadow: 0 0 2px black;
}
.dino-call-window .call-header-background {
.dino-call-window .participant-header-bar {
background: linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0));
border: 0;
border-radius: 0;
}
.dino-call-window .participant-header-bar button {
background: none;
}
.dino-call-window .participant-header-bar button:hover {
.dino-call-window .participant-header-bar button:hover:not(.close) {
background: rgba(255,255,255,0.15);
border-color: rgba(255,255,255,0);
box-shadow: none;

View File

@ -103,6 +103,9 @@
<property name="hexpand">false</property>
<property name="maximum-size">400</property>
<property name="tightening-threshold">400</property>
<child>
<object class="DinoUiNaturalSizeIncrease">
<property name="min-natural-width">400</property>
<child>
<object class="AdwBin" id="search_frame">
<property name="hexpand">true</property>
@ -118,4 +121,6 @@
</object>
</child>
</object>
</child>
</object>
</interface>

View File

@ -1052,7 +1052,7 @@ msgid "Check spelling"
msgstr "التدقيق الإملائي"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "تطبيق حديث للدردشة عبر XMPP"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1035,7 +1035,7 @@ msgid "Check spelling"
msgstr "Revisa l'ortografia"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "Client de xat XMPP modern"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1038,7 +1038,7 @@ msgid "Check spelling"
msgstr "Kontrola pravopisu"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "Moderní XMPP klient"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1034,7 +1034,7 @@ msgid "Check spelling"
msgstr ""
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr ""
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1033,7 +1033,7 @@ msgid "Check spelling"
msgstr "Rechtschreibung prüfen"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "Modernes XMPP-Chat-Programm"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1035,7 +1035,7 @@ msgid "Check spelling"
msgstr ""
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr ""
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1040,7 +1040,7 @@ msgid "Check spelling"
msgstr "Ορθογραφικός έλεγχος"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "Σύγχρονος XMPP Chat Client"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1025,7 +1025,7 @@ msgid "Check spelling"
msgstr ""
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr ""
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1035,7 +1035,7 @@ msgid "Check spelling"
msgstr "Kontroli ortografion"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "Moderna XMPP-Retebabililo"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1038,7 +1038,7 @@ msgid "Check spelling"
msgstr "Comprobar ortografía"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "Un cliente de XMPP moderno"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1037,7 +1037,7 @@ msgid "Check spelling"
msgstr "Ortografia egiaztatu"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "XMPP txat bezero modernoa"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1034,7 +1034,7 @@ msgid "Check spelling"
msgstr "بررسی املا"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "کلاینت نوین گپ XMPP"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1036,7 +1036,7 @@ msgstr ""
#: main/data/im.dino.Dino.appdata.xml.in:7
#, fuzzy
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "Moderni XMPP-asiakasohjelma"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1038,7 +1038,7 @@ msgid "Check spelling"
msgstr "Vérifier l'orthographe"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "Client de clavardage XMPP moderne"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1038,7 +1038,7 @@ msgid "Check spelling"
msgstr "Comprobar ortografía"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "Cliente moderno para conversas XMPP"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1041,7 +1041,7 @@ msgid "Check spelling"
msgstr "Helyesírás-ellenőrzés"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "Modern XMPP csevegőprogram"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1030,7 +1030,7 @@ msgid "Check spelling"
msgstr "Periksa ejaan"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "Aplikasi chat XMPP modern"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1034,7 +1034,7 @@ msgid "Check spelling"
msgstr ""
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "Un modern client de conversationes XMPP"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1035,7 +1035,7 @@ msgid "Check spelling"
msgstr "Kanna stafsetningu"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "Nútímalegt XMPP-spjallforrit"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1037,7 +1037,7 @@ msgid "Check spelling"
msgstr "Controlla l'ortografia"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "Client di chat moderno per XMPP"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1032,7 +1032,7 @@ msgid "Check spelling"
msgstr "スペルチェック"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "現代的な XMPP チャット クライアント"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1034,7 +1034,7 @@ msgid "Check spelling"
msgstr ""
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr ""
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1038,7 +1038,7 @@ msgid "Check spelling"
msgstr "맞춤법 확인"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "현대 XMPP 채팅 클라이언트"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1030,7 +1030,7 @@ msgid "Check spelling"
msgstr ""
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "Modernen XMPP Chat Client"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1041,7 +1041,7 @@ msgid "Check spelling"
msgstr "Tikrinti rašybą"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "Šiuolaikinė XMPP pokalbių kliento programa"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1035,7 +1035,7 @@ msgid "Check spelling"
msgstr ""
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr ""
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1039,7 +1039,7 @@ msgid "Check spelling"
msgstr "Stavekontroll"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "Moderne XMPP-sludreklient"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1035,7 +1035,7 @@ msgid "Check spelling"
msgstr "Spelling controleren"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "Een moderne XMPP-chatclient"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1037,7 +1037,7 @@ msgid "Check spelling"
msgstr "Verificar lortografia"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "Client XMPP modèrn"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1042,7 +1042,7 @@ msgid "Check spelling"
msgstr "Sprawdź pisownie"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "Nowoczesny komunikator XMPP"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1035,7 +1035,7 @@ msgid "Check spelling"
msgstr "Verificar ortografia"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "Moderno cliente de chat XMPP"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1036,7 +1036,7 @@ msgid "Check spelling"
msgstr "Verificar ortografia"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "Cliente de Chat XMPP Moderno"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1042,7 +1042,7 @@ msgid "Check spelling"
msgstr "Verificare ortografie"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "Client XMPP de discuții modern"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1040,7 +1040,7 @@ msgid "Check spelling"
msgstr "Проверка орфографии"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "Современный XMPP клиент"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1035,7 +1035,7 @@ msgid "Check spelling"
msgstr "Kontroll drejtshkrimi"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "Klient Modern Fjalosjesh XMPP"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1035,7 +1035,7 @@ msgid "Check spelling"
msgstr "Kontrollera stavning"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "Modern XMPP-chattklient"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1034,7 +1034,7 @@ msgid "Check spelling"
msgstr ""
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr ""
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1034,7 +1034,7 @@ msgid "Check spelling"
msgstr "Yazım denetimi"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "Modern XMPP Sohbet İstemcisi"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1039,7 +1039,7 @@ msgid "Check spelling"
msgstr ""
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr ""
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1031,7 +1031,7 @@ msgid "Check spelling"
msgstr "检查拼写"
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "现代 XMPP 聊天客户端"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -1030,7 +1030,7 @@ msgid "Check spelling"
msgstr ""
#: main/data/im.dino.Dino.appdata.xml.in:7
msgid "Modern XMPP Chat Client"
msgid "Modern XMPP chat client"
msgstr "現代化的 XMPP 用戶端聊天軟件"
#: main/data/im.dino.Dino.appdata.xml.in:9

View File

@ -101,6 +101,7 @@ public class AddConferenceDialog : Gtk.Dialog {
});
select_fragment.remove_jid.connect((row) => {
ConferenceListRow conference_row = row as ConferenceListRow;
if (conference_row == null) return;
stream_interactor.get_module(MucManager.IDENTITY).remove_bookmark(conference_row.account, conference_row.bookmark);
});
@ -162,7 +163,7 @@ public class AddConferenceDialog : Gtk.Dialog {
details_fragment.clear();
ListRow? row = conference_list_box.get_selected_row() != null ? conference_list_box.get_selected_row().get_child() as ListRow : null;
ConferenceListRow? conference_row = conference_list_box.get_selected_row() != null ? conference_list_box.get_selected_row() as ConferenceListRow : null;
ConferenceListRow? conference_row = conference_list_box.get_selected_row() != null ? conference_list_box.get_selected_row().get_child() as ConferenceListRow : null;
if (conference_row != null) {
details_fragment.account = conference_row.account;
details_fragment.jid = conference_row.bookmark.jid.to_string();

View File

@ -138,7 +138,7 @@ public class Dino.Ui.CallBottomBar : Gtk.Box {
public void show_counterpart_ended(string text) {
stack.set_visible_child_name("label");
label.label = text;
label.label = Util.unbreak_space_around_non_spacing_mark(text);
}
public bool is_menu_active() {

View File

@ -22,7 +22,12 @@ namespace Dino.Ui {
private HashMap<string, ParticipantWidget> participant_widgets = new HashMap<string, ParticipantWidget>();
private ArrayList<string> participants = new ArrayList<string>();
private EventControllerFocus this_focus_events = new EventControllerFocus();
private GestureClick this_gesture_events = new GestureClick() { touch_only=true, propagation_phase=Gtk.PropagationPhase.CAPTURE };
private EventControllerMotion this_motion_events = new EventControllerMotion();
private double latest_motion_x = -1;
private double latest_motion_y = -1;
private const double MOTION_RELEVANCE_THRESHOLD = 2;
private int own_video_width = 150;
private int own_video_height = 100;
@ -54,9 +59,20 @@ namespace Dino.Ui {
this.bind_property("controls-active", bottom_bar_revealer, "reveal-child", BindingFlags.SYNC_CREATE);
((Widget) this).add_controller(this_motion_events);
this_motion_events.motion.connect(reveal_control_elements);
this_motion_events.enter.connect(reveal_control_elements);
this_motion_events.leave.connect(reveal_control_elements);
this_motion_events.motion.connect((x, y) => {
if ((latest_motion_x - x).abs() <= MOTION_RELEVANCE_THRESHOLD && (latest_motion_y - y).abs() <= MOTION_RELEVANCE_THRESHOLD) return;
latest_motion_x = x;
latest_motion_y = y;
reveal_control_elements();
});
((Widget) this).add_controller(this_focus_events);
this_focus_events.enter.connect(reveal_control_elements);
this_focus_events.leave.connect(reveal_control_elements);
((Widget) this).add_controller(this_gesture_events);
this_gesture_events.pressed.connect_after(reveal_control_elements);
this.notify["default-width"].connect(reveal_control_elements);
this.notify["default-height"].connect(reveal_control_elements);

View File

@ -74,14 +74,11 @@ namespace Dino.Ui {
header_bar.show_title_buttons = is_highest_row;
if (is_highest_row) {
header_bar.add_css_class("call-header-background");
Gtk.Settings? gtk_settings = Gtk.Settings.get_default();
if (gtk_settings != null) {
string[] buttons = gtk_settings.gtk_decoration_layout.split(":");
header_bar.decoration_layout = (is_start ? buttons[0] : "") + ":" + (is_end && buttons.length == 2 ? buttons[1] : "");
}
} else {
header_bar.remove_css_class("call-header-background");
}
reveal_or_hide_controls();
}

View File

@ -94,10 +94,16 @@ public class ChatTextView : Box {
}
}
private bool on_text_input_key_press(uint keyval, uint keycode, Gdk.ModifierType state) {
private bool on_text_input_key_press(EventControllerKey controller, uint keyval, uint keycode, Gdk.ModifierType state) {
if (keyval in new uint[]{ Key.Return, Key.KP_Enter }) {
// Allow the text view to process the event. Needed for IME.
if (text_view.im_context_filter_keypress(controller.get_current_event())) {
return true;
}
if ((state & ModifierType.SHIFT_MASK) > 0) {
text_view.buffer.insert_at_cursor("\n", 1);
// Let the default handler normally insert a newline if shift was hold
return false;
} else if (text_view.buffer.text.strip() != "") {
send_text();
}

View File

@ -39,6 +39,8 @@ public class View : Box {
chooser.emoji_picked.connect((emoji) => {
chat_text_view.text_view.buffer.insert_at_cursor(emoji, emoji.data.length);
});
chooser.closed.connect(do_focus);
emoji_button.set_popover(chooser);
file_button.tooltip_text = Util.string_if_tooltips_active(_("Send a file"));

View File

@ -18,7 +18,7 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
[GtkChild] private unowned Box message_menu_box;
[GtkChild] private unowned Box notifications;
[GtkChild] private unowned Box main;
[GtkChild] private unowned Box main_wrap_box;
[GtkChild] private unowned Widget main_wrap_box;
private HashMap<string, Widget> action_buttons = new HashMap<string, Widget>();
private Gee.List<Dino.Plugins.MessageAction>? message_actions = null;
@ -46,6 +46,7 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
construct {
this.layout_manager = new BinLayout();
main_wrap_box.layout_manager = new BinLayout();
// Setup all message menu buttons
var correction_button = new Button() { name="correction" };
@ -95,7 +96,6 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
EventControllerMotion main_wrap_motion_events = new EventControllerMotion();
main_wrap_box.add_controller(main_wrap_motion_events);
main_wrap_motion_events.leave.connect(on_leave_notify_event);
main_wrap_motion_events.enter.connect(update_highlight);
// The buttons of the overlaying message_menu_box may partially overlap the adjacent
// conversation items. We connect to the main_event_box directly to avoid emitting
// the pointer motion events as long as the pointer is above the message menu.
@ -607,6 +607,12 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
widget.dispose();
}
widgets.clear();
Widget? notification = notifications.get_first_child();
while (notification != null) {
notifications.remove(notification);
notification = notifications.get_first_child();
}
}
private void clear_notifications() {

View File

@ -6,7 +6,11 @@ using Dino.Entities;
namespace Dino.Ui {
public class FileImageWidget : Box {
public class FileImageWidget : Widget {
construct {
layout_manager = new BinLayout();
}
public FileImageWidget() {
this.halign = Align.START;
@ -74,7 +78,12 @@ public class FileImageWidget : Box {
image_overlay_toolbar.visible = false;
});
this.append(overlay);
overlay.insert_after(this, null);
}
public override void dispose() {
if (get_first_child() != null && get_first_child().parent != null) get_first_child().unparent();
base.dispose();
}
}

View File

@ -39,7 +39,7 @@ public class FileMetaItem : ConversationSummary.ContentMetaItem {
}
}
public class FileWidget : SizeRequestBox {
public class FileWidget : SizeRequestBin {
enum State {
IMAGE,
@ -100,22 +100,22 @@ public class FileWidget : SizeRequestBox {
// If the widget changed in the meanwhile, stop
if (content != content_bak) return;
if (content != null) this.remove(content);
if (content != null) content.unparent();
content = file_image_widget;
state = State.IMAGE;
this.append(content);
content.insert_after(this, null);
return;
} catch (Error e) { }
}
if (state != State.DEFAULT) {
if (content != null) this.remove(content);
if (content != null) content.unparent();
FileDefaultWidget default_file_widget = new FileDefaultWidget();
default_widget_controller = new FileDefaultWidgetController(default_file_widget);
default_widget_controller.set_file_transfer(file_transfer);
content = default_file_widget;
this.state = State.DEFAULT;
this.append(content);
content.insert_after(this, null);
}
}

View File

@ -87,6 +87,9 @@ public class MessageMetaItem : ContentMetaItem {
markup_text = markup_text.substring(4);
}
// Work around pango bug
markup_text = Util.unbreak_space_around_non_spacing_mark((owned) markup_text);
if (conversation.type_ == Conversation.Type.GROUPCHAT) {
markup_text = Util.parse_add_markup_theme(markup_text, conversation.nickname, true, true, true, Util.is_dark_theme(this.label), ref theme_dependent);
} else {

View File

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

View File

@ -223,15 +223,16 @@ public class GlobalSearch {
grid.margin_top = 3;
grid.margin_bottom = 3;
string text = item.message.body.replace("\n", "").replace("\r", "");
if (text.length > 200) {
string text = Util.unbreak_space_around_non_spacing_mark(item.message.body.replace("\n", "").replace("\r", ""));
if (text.char_count() > 200) {
int index = text.index_of(search);
if (index + search.length <= 100) {
text = text.substring(0, 150) + "" + text.substring(text.length - 50, 50);
} else if (index >= text.length - 100) {
text = text.substring(0, 50) + "" + text.substring(text.length - 150, 150);
int char_index = index < 0 ? 0 : text.char_count(index);
if (char_index + search.char_count() <= 100) {
text = text.substring(0, text.index_of_nth_char(150)) + "" + text.substring(text.index_of_nth_char(text.char_count() - 50));
} else if (char_index >= text.char_count() - 100) {
text = text.substring(0, text.index_of_nth_char(50)) + "" + text.substring(text.index_of_nth_char(text.char_count() - 150));
} else {
text = text.substring(0, 25) + "" + text.substring(index - 50, 50) + text.substring(index, 100) + "" + text.substring(text.length - 25, 25);
text = text.substring(0, text.index_of_nth_char(25)) + "" + text.substring(text.index_of_nth_char(char_index - 50), text.index_of_nth_char(char_index + 100)) + "" + text.substring(text.index_of_nth_char(text.char_count() - 25));
}
}
Label label = new Label("") { use_markup=true, xalign=0, selectable=true, wrap=true, wrap_mode=Pango.WrapMode.WORD_CHAR, vexpand=true };
@ -260,7 +261,8 @@ public class GlobalSearch {
for (; match_info.matches(); match_info.next()) {
int start, end;
match_info.fetch_pos(0, out start, out end);
markup_text += Markup.escape_text(text[last_end:start]) + "<span bgcolor=\"yellow\">" + Markup.escape_text(text[start:end]) + "</span>";
string themed_span = Util.is_dark_theme(label) ? "<span color=\"black\" bgcolor=\"yellow\">" : "<span bgcolor=\"yellow\">";
markup_text += Markup.escape_text(text[last_end:start]) + themed_span + Markup.escape_text(text[start:end]) + "</span>";
last_end = end;
}
markup_text += Markup.escape_text(text[last_end:text.length]);

View File

@ -345,7 +345,7 @@ public class AddAccountDialog : Gtk.Dialog {
register_form_continue.grab_focus();
} else if (form.fields.size > 0) {
if (form.instructions != null && form.instructions != "") {
string markup_instructions = Util.parse_add_markup(form.instructions, null, true, false);
string markup_instructions = Util.parse_add_markup(Util.unbreak_space_around_non_spacing_mark(form.instructions), null, true, false);
form_box.append(new Label(markup_instructions) { use_markup=true, halign=Align.CENTER, xalign=0, margin_top=7,
wrap=true, wrap_mode=Pango.WrapMode.WORD_CHAR });
}

View File

@ -267,7 +267,6 @@ public class Dino.Ui.FreeDesktopNotifier : NotificationProvider, Object {
}
public async void retract_content_item_notifications() {
if (content_notifications != null) {
foreach (uint32 id in content_notifications.values) {
try {
dbus_notifications.close_notification.begin(id);
@ -275,17 +274,23 @@ public class Dino.Ui.FreeDesktopNotifier : NotificationProvider, Object {
}
content_notifications.clear();
}
}
public async void retract_conversation_notifications(Conversation conversation) {
if (content_notifications.has_key(conversation)) {
try {
if (content_notifications.has_key(conversation)) {
dbus_notifications.close_notification.begin(content_notifications[conversation]);
} catch (Error e) { }
}
content_notifications.unset(conversation);
}
if (conversation_notifications.has_key(conversation)) {
foreach (var notification_id in conversation_notifications[conversation]) {
dbus_notifications.close_notification.begin(notification_id);
}
conversation_notifications.unset(conversation);
}
} catch (Error e) { }
}
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();

View File

@ -238,6 +238,41 @@ public static Map<unichar, unichar> get_matching_chars() {
return MATCHING_CHARS;
}
/**
* This replaces spaces with non-breaking spaces when they are adjacent to a non-spacing mark.
*
* We do this to work-around a bug in Pango. See https://gitlab.gnome.org/GNOME/pango/-/issues/798 and
* https://gitlab.gnome.org/GNOME/pango/-/issues/832
*
* This is zero-copy iff no space is adjacent to a non-spacing mark, otherwise the provided string will be destroyed
* and the returned string should be used instead.
*/
public static string unbreak_space_around_non_spacing_mark(owned string s) {
int current_index = 0;
unichar current_char = 0;
int prev_index = 0;
unichar prev_char = 0;
bool is_non_spacing_mark = false;
while (s.get_next_char(ref current_index, out current_char)) {
int replace_index = -1;
if (is_non_spacing_mark && current_char == ' ') {
replace_index = prev_index;
current_char = ' ';
}
is_non_spacing_mark = ICU.get_int_property_value(current_char, ICU.Property.BIDI_CLASS) == ICU.CharDirection.DIR_NON_SPACING_MARK;
if (prev_char == ' ' && is_non_spacing_mark) {
replace_index = prev_index - 1;
}
if (replace_index != -1) {
s = s[0:replace_index] + " " + s[(replace_index + 1):s.length];
current_index += 1;
}
prev_index = current_index;
prev_char = current_char;
}
return (owned) s;
}
public static string parse_add_markup(string s_, string? highlight_word, bool parse_links, bool parse_text_markup) {
bool ignore_out_var = false;
return parse_add_markup_theme(s_, highlight_word, parse_links, parse_text_markup, parse_text_markup, false, ref ignore_out_var);

View File

@ -16,6 +16,17 @@ public class SizeRequestBin : Widget {
this.layout_manager = new BinLayout();
}
public override void compute_expand_internal(out bool hexpand, out bool vexpand) {
hexpand = false;
vexpand = false;
Widget child = get_first_child();
while (child != null) {
hexpand = hexpand || child.compute_expand(Orientation.HORIZONTAL);
vexpand = vexpand || child.compute_expand(Orientation.VERTICAL);
child = child.get_next_sibling();
}
}
public override Gtk.SizeRequestMode get_request_mode() {
return size_request_mode;
}

View File

@ -9,29 +9,72 @@ public class SizingBin : Widget {
public int target_height { get; set; default = -1; }
public int max_height { get; set; default = -1; }
construct {
layout_manager = new BinLayout();
public override void compute_expand_internal(out bool hexpand, out bool vexpand) {
hexpand = false;
vexpand = false;
Widget child = get_first_child();
while (child != null) {
hexpand = hexpand || child.compute_expand(Orientation.HORIZONTAL);
vexpand = vexpand || child.compute_expand(Orientation.VERTICAL);
child = child.get_next_sibling();
}
}
public override void size_allocate(int width, int height, int baseline) {
if (max_height != -1) height = int.min(height, max_height);
if (max_width != -1) width = int.min(width, max_width);
base.size_allocate(width, height, baseline);
Widget child = get_first_child();
while (child != null) {
if (child.should_layout()) {
child.allocate(width, height, baseline, null);
}
child = child.get_next_sibling();
}
}
public override void measure(Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) {
base.measure(orientation, for_size, out minimum, out natural, out minimum_baseline, out natural_baseline);
if (orientation == Orientation.HORIZONTAL) {
if (min_width != -1) minimum = int.max(minimum, min_width);
if (max_width != -1) natural = int.min(natural, max_width);
if (target_width != -1) natural = int.max(natural, target_width);
natural = int.max(natural, minimum);
minimum = min_width;
natural = target_width;
} else {
minimum = min_height;
natural = target_height;
}
minimum_baseline = -1;
natural_baseline = -1;
Widget child = get_first_child();
while (child != null) {
if (child.should_layout()) {
int child_min = 0;
int child_nat = 0;
int child_min_baseline = -1;
int child_nat_baseline = -1;
child.measure(orientation, for_size, out child_min, out child_nat, out child_min_baseline, out child_nat_baseline);
minimum = int.max(minimum, child_min);
natural = int.max(natural, child_nat);
if (child_min_baseline > 0) {
minimum_baseline = int.max(minimum_baseline, child_min_baseline);
}
if (child_nat_baseline > 0) {
natural_baseline = int.max(natural_baseline, child_nat_baseline);
}
}
child = child.get_next_sibling();
}
if (orientation == Orientation.HORIZONTAL) {
if (max_width != -1) natural = int.min(natural, max_width);
} else {
if (min_height != -1) minimum = int.max(minimum, min_height);
if (max_height != -1) natural = int.min(natural, max_height);
if (target_height != -1) natural = int.max(natural, target_height);
}
natural = int.max(natural, minimum);
}
public override SizeRequestMode get_request_mode() {
Widget child = get_first_child();
if (child != null) {
return child.get_request_mode();
}
return SizeRequestMode.CONSTANT_SIZE;
}
public override void dispose() {

View File

@ -62,12 +62,12 @@ class Dino.Ui.FixedRatioPicture : Gtk.Widget {
measure_target_size(out width, out height);
if (orientation == Orientation.HORIZONTAL) {
minimum = min_width;
natural = width;
natural = int.max(min_width, int.min(width, max_width));
} else if (for_size == -1) {
minimum = min_height;
natural = height;
natural = int.max(min_height, int.min(height, max_height));
} else {
minimum = natural = height * for_size / width;
minimum = natural = int.max(min_height, int.min(height * for_size / width, height));
}
}

View File

@ -1,21 +1,31 @@
using Gtk;
public class Dino.Ui.NaturalSizeIncrease : Gtk.Widget {
public int min_natural_height { get; set; default = -1; }
public int min_natural_width { get; set; default = -1; }
public uint min_natural_height { get; set; default = 0; }
public uint min_natural_width { get; set; default = 0; }
construct {
this.notify.connect(queue_resize);
}
public override void compute_expand_internal(out bool hexpand, out bool vexpand) {
hexpand = false;
vexpand = false;
Widget child = get_first_child();
while (child != null) {
hexpand = hexpand || child.compute_expand(Orientation.HORIZONTAL);
vexpand = vexpand || child.compute_expand(Orientation.VERTICAL);
child = child.get_next_sibling();
}
}
public override void measure(Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) {
minimum = 0;
if (orientation == Orientation.HORIZONTAL) {
natural = min_natural_width;
natural = (int) min_natural_width;
} else {
natural = min_natural_height;
natural = (int) min_natural_height;
}
natural = int.max(0, natural);
minimum_baseline = -1;
natural_baseline = -1;
Widget child = get_first_child();
@ -25,7 +35,7 @@ public class Dino.Ui.NaturalSizeIncrease : Gtk.Widget {
int child_nat = 0;
int child_min_baseline = -1;
int child_nat_baseline = -1;
child.measure(orientation, -1, out child_min, out child_nat, out child_min_baseline, out child_nat_baseline);
child.measure(orientation, for_size, out child_min, out child_nat, out child_min_baseline, out child_nat_baseline);
minimum = int.max(minimum, child_min);
natural = int.max(natural, child_nat);
if (child_min_baseline > 0) {
@ -56,4 +66,9 @@ public class Dino.Ui.NaturalSizeIncrease : Gtk.Widget {
}
return SizeRequestMode.CONSTANT_SIZE;
}
public override void dispose() {
var child = this.get_first_child();
if (child != null) child.unparent();
}
}

View File

@ -1,14 +1,23 @@
namespace ICU {
[CCode (cprefix = "UCHAR_", cheader_filename = "unicode/uchar.h")]
[CCode (cname = "UProperty", cprefix = "UCHAR_", has_type_id = false, cheader_filename = "unicode/uchar.h")]
public enum Property {
EMOJI,
EMOJI_PRESENTATION,
EMOJI_MODIFIER,
EMOJI_MODIFIER_BASE,
BIDI_CLASS,
}
[CCode (cname = "UCharDirection", cprefix = "U_", has_type_id = false, cheader_filename = "unicode/uchar.h")]
public enum CharDirection {
DIR_NON_SPACING_MARK,
}
[CCode (cname = "u_hasBinaryProperty", cheader_filename = "unicode/uchar.h")]
public bool has_binary_property(unichar c, Property p);
[CCode (cname = "u_getIntPropertyValue", cheader_filename = "unicode/uchar.h")]
public int32 get_int_property_value(unichar c, Property p);
}

View File

@ -117,6 +117,7 @@ public static Gee.List<Key> get_keylist(string? pattern = null, bool secret_only
} catch (Error e) {
if (e.code != GPGError.ErrorCode.EOF) throw e;
}
context.op_keylist_end();
return keys;
} finally {
global_mutex.unlock();

View File

@ -22,9 +22,9 @@ public class Key {
public string issuer_name;
public string chain_id;
public Validity owner_trust;
[CCode(array_null_terminated = true)]
[CCode (array_length = false, array_null_terminated = true)]
public SubKey[] subkeys;
[CCode(array_null_terminated = true)]
[CCode (array_length = false, array_null_terminated = true)]
public UserID[] uids;
public KeylistMode keylist_mode;
// public string fpr; // requires gpgme >= 1.7.0

View File

@ -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));
}
@ -114,11 +117,14 @@ 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");
#if SOUP_3_0
string transfer_host = Uri.parse(http_receive_data.url, UriFlags.NONE).get_host();
head_message.accept_certificate.connect((peer_cert, errors) => { return ConnectionManager.on_invalid_certificate(transfer_host, peer_cert, errors); });
#endif
try {
#if SOUP_3_0
yield session.send_async(head_message, GLib.Priority.LOW, null);
@ -150,10 +156,13 @@ 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);
#if SOUP_3_0
string transfer_host = Uri.parse(http_receive_data.url, UriFlags.NONE).get_host();
get_message.accept_certificate.connect((peer_cert, errors) => { return ConnectionManager.on_invalid_certificate(transfer_host, peer_cert, errors); });
#endif
try {
#if SOUP_3_0
InputStream stream = yield session.send_async(get_message, GLib.Priority.LOW, file_transfer.cancellable);

View File

@ -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<Account, long> max_file_sizes = new HashMap<Account, long>(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,10 +93,11 @@ 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
string transfer_host = Uri.parse(file_send_data.url_up, UriFlags.NONE).get_host();
put_message.accept_certificate.connect((peer_cert, errors) => { return ConnectionManager.on_invalid_certificate(transfer_host, peer_cert, errors); });
put_message.set_request_body(file_meta.mime_type, file_transfer.input_stream, (ssize_t) file_meta.size);
#else
put_message.request_headers.set_content_type(file_meta.mime_type, null);

View File

@ -38,7 +38,11 @@ public class Handler {
}
public uint8[]? process_incoming_data(uint component_id, uint8[] data) throws Crypto.Error {
if (srtp_session.has_decrypt) {
if (data[0] >= 128) {
if (!srtp_session.has_decrypt) {
debug("Received data before SRTP session is ready, dropping.");
return null;
}
if (component_id == 1) {
if (data.length >= 2 && data[1] >= 192 && data[1] < 224) {
return srtp_session.decrypt_rtcp(data);
@ -46,9 +50,12 @@ public class Handler {
return srtp_session.decrypt_rtp(data);
}
if (component_id == 2) return srtp_session.decrypt_rtcp(data);
} else if (component_id == 1) {
on_data_rec(data);
}
if (component_id == 1 && data.length >= 1 && (data[0] >= 20 && data[0] < 64)) {
on_data_rec(data);
return null;
}
debug("Dropping unknown data from component %u", component_id);
return null;
}
@ -79,7 +86,7 @@ public class Handler {
err = private_key.generate(PKAlgorithm.ECDSA, 256);
throw_if_error(err);
var start_time = new DateTime.now_local().add_days(1);
var start_time = new DateTime.now_local().add_days(-1);
var end_time = start_time.add_days(2);
X509.Certificate cert = X509.Certificate.create();

View File

@ -6,6 +6,8 @@ using Xmpp.Xep;
private extern const size_t NICE_ADDRESS_STRING_LEN;
public class Dino.Plugins.Ice.Plugin : RootInterface, Object {
private HashMap<Account, uint> timeouts = new HashMap<Account, uint>(Account.hash_func, Account.equals_func);
public Dino.Application app;
public void registered(Dino.Application app) {
@ -22,18 +24,24 @@ public class Dino.Plugins.Ice.Plugin : RootInterface, Object {
stream.get_module(JingleRawUdp.Module.IDENTITY).set_local_ip_address_handler(get_local_ip_addresses);
}
});
app.stream_interactor.stream_negotiated.connect(on_stream_negotiated);
app.stream_interactor.stream_negotiated.connect(external_discovery_refresh_services);
}
private async void on_stream_negotiated(Account account, XmppStream stream) {
private async void external_discovery_refresh_services(Account account, XmppStream stream) {
Module? ice_udp_module = stream.get_module(JingleIceUdp.Module.IDENTITY) as Module;
if (ice_udp_module == null) return;
Gee.List<Xep.ExternalServiceDiscovery.Service> services = yield ExternalServiceDiscovery.request_services(stream);
foreach (Xep.ExternalServiceDiscovery.Service service in services) {
if (service.transport == "udp" && (service.ty == "stun" || service.ty == "turn")) {
InetAddress ip = yield lookup_ipv4_addess(service.host);
InetAddress? ip = yield lookup_ipv4_addess(service.host);
if (ip == null) continue;
if (ip.is_any || ip.is_link_local || ip.is_loopback || ip.is_multicast || ip.is_site_local) {
warning("Ignoring STUN/TURN server at %s", service.host);
continue;
}
if (service.ty == "stun") {
debug("Server offers STUN server: %s:%u, resolved to %s", service.host, service.port, ip.to_string());
ice_udp_module.stun_ip = ip.to_string();
@ -54,6 +62,26 @@ public class Dino.Plugins.Ice.Plugin : RootInterface, Object {
ice_udp_module.stun_ip = ip.to_string();
ice_udp_module.stun_port = 7886;
}
// Refresh TURN credentials before they expire
if (ice_udp_module.turn_service != null) {
DateTime? expires = ice_udp_module.turn_service.expires;
if (expires != null) {
uint refresh_delay = (uint) (expires.to_unix() - new DateTime.now_utc().to_unix()) / 2;
if (refresh_delay >= 300 && refresh_delay <= (int64) uint.MAX) { // 5 min < refetch < max
debug("Re-fetching TURN credentials in %u sec", refresh_delay);
timeouts.unset(account);
timeouts[account] = Timeout.add_seconds((uint) refresh_delay, () => {
external_discovery_refresh_services.begin(account, stream);
return false;
});
} else {
warning("TURN credentials' expiry time = %u, *not* re-fetching", refresh_delay);
}
}
}
}
public void shutdown() {

View File

@ -28,9 +28,6 @@ namespace Dino.Plugins.Omemo {
StanzaNode? encrypted_node = stanza.stanza.get_subnode("encrypted", NS_URI);
if (encrypted_node == null || MessageFlag.get_flag(stanza) != null || stanza.from == null) return false;
if (message.body == null && Xep.ExplicitEncryption.get_encryption_tag(stanza) == NS_URI) {
message.body = "[This message is OMEMO encrypted]"; // TODO temporary
}
if (!Plugin.ensure_context()) return false;
int identity_id = db.identity.get_id(conversation.account.id);
@ -38,7 +35,7 @@ namespace Dino.Plugins.Omemo {
stanza.add_flag(flag);
Xep.Omemo.ParsedData? data = parse_node(encrypted_node);
if (data == null || data.ciphertext == null) return false;
if (data == null) return false;
foreach (Bytes encr_key in data.our_potential_encrypted_keys.keys) {
@ -52,14 +49,16 @@ namespace Dino.Plugins.Omemo {
foreach (Jid possible_jid in possible_jids) {
try {
uint8[] key = decrypt_key(data, possible_jid);
if (data.ciphertext != null) {
string cleartext = arr_to_str(aes_decrypt(Cipher.AES_GCM_NOPADDING, key, data.iv, data.ciphertext));
message.body = cleartext;
}
// If we figured out which real jid a message comes from due to decryption working, save it
if (conversation.type_ == Conversation.Type.GROUPCHAT && message.real_jid == null) {
message.real_jid = possible_jid;
}
message.body = cleartext;
message.encryption = Encryption.OMEMO;
trust_manager.message_device_id_map[message] = data.sid;
@ -71,7 +70,7 @@ namespace Dino.Plugins.Omemo {
}
if (
encrypted_node.get_deep_string_content("payload") != null && // Ratchet forwarding doesn't contain payload and might not include us, which is ok
data.ciphertext != null && // Ratchet forwarding doesn't contain payload and might not include us, which is ok
data.our_potential_encrypted_keys.size == 0 && // The message was not encrypted to us
stream_interactor.module_manager.get_module(message.account, StreamModule.IDENTITY).store.local_registration_id != data.sid // Message from this device. Never encrypted to itself.
) {

View File

@ -66,6 +66,7 @@ public class Manager : StreamInteractionModule, Object {
this.trust_manager = trust_manager;
this.encryptors = encryptors;
stream_interactor.account_added.connect(on_account_added);
stream_interactor.stream_negotiated.connect(on_stream_negotiated);
stream_interactor.get_module(MessageProcessor.IDENTITY).pre_message_send.connect(on_pre_message_send);
stream_interactor.get_module(RosterManager.IDENTITY).mutual_subscription.connect(on_mutual_subscription);
@ -182,6 +183,12 @@ public class Manager : StreamInteractionModule, Object {
StreamModule module = stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY);
if (module != null) {
module.request_user_devicelist.begin(stream, account.bare_jid);
}
}
private void on_account_added(Account account) {
StreamModule module = stream_interactor.module_manager.get_module(account, StreamModule.IDENTITY);
if (module != null) {
module.device_list_loaded.connect((jid, devices) => on_device_list_loaded(account, jid, devices));
module.bundle_fetched.connect((jid, device_id, bundle) => on_bundle_fetched(account, jid, device_id, bundle));
module.bundle_fetch_failed.connect((jid) => continue_message_sending(account, jid));

View File

@ -117,7 +117,9 @@ public class AccountSettingsEntry : Plugins.AccountSettingsEntry {
new Thread<void*> (null, () => { // Querying GnuPG might take some time
try {
keys = GPGHelper.get_keylist(null, true);
} catch (Error e) { }
} catch (Error e) {
warning(e.message);
}
Idle.add((owned)callback);
return null;
});

View File

@ -53,16 +53,25 @@ if(RTP_ENABLE_MSDK)
set(RTP_DEFINITIONS ${RTP_DEFINITIONS} ENABLE_MSDK)
endif()
if(WebRTCAudioProcessing_VERSION GREATER "0.4")
message(STATUS "Ignoring WebRTCAudioProcessing, only versions < 0.4 supported so far")
if(WebRTCAudioProcessing_VERSION GREATER_EQUAL "3.0")
message(STATUS "Ignoring WebRTCAudioProcessing, only versions 0.2+, 1.* and 2.* supported so far")
unset(WebRTCAudioProcessing_FOUND)
endif()
if(WebRTCAudioProcessing_FOUND)
set(RTP_DEFINITIONS ${RTP_DEFINITIONS} WITH_VOICE_PROCESSOR)
if(WebRTCAudioProcessing_VERSION GREATER_EQUAL "2.0")
set(RTP_VOICE_PROCESSOR_CFLAGS "-DWEBRTC2")
set(RTP_VOICE_PROCESSOR_LIB webrtc-audio-processing-2)
elseif(WebRTCAudioProcessing_VERSION GREATER_EQUAL "1.0")
set(RTP_VOICE_PROCESSOR_CFLAGS "-DWEBRTC1")
set(RTP_VOICE_PROCESSOR_LIB webrtc-audio-processing-1)
else()
set(RTP_VOICE_PROCESSOR_CFLAGS "-DWEBRTC0")
set(RTP_VOICE_PROCESSOR_LIB webrtc-audio-processing)
endif()
set(RTP_VOICE_PROCESSOR_VALA src/voice_processor.vala)
set(RTP_VOICE_PROCESSOR_CXX src/voice_processor_native.cpp)
set(RTP_VOICE_PROCESSOR_LIB webrtc-audio-processing)
else()
message(STATUS "WebRTCAudioProcessing not found, build without voice pre-processing!")
endif()
@ -90,7 +99,7 @@ OPTIONS
${RTP_EXTRA_OPTIONS}
)
add_definitions(${VALA_CFLAGS} -DG_LOG_DOMAIN="rtp" -I${CMAKE_CURRENT_SOURCE_DIR}/src)
add_definitions(${VALA_CFLAGS} -DG_LOG_DOMAIN="rtp" -I${CMAKE_CURRENT_SOURCE_DIR}/src ${RTP_VOICE_PROCESSOR_CFLAGS})
add_library(rtp SHARED ${RTP_VALA_C} ${RTP_VOICE_PROCESSOR_CXX} src/gst_fixes.c)
target_link_libraries(rtp libdino crypto-vala ${RTP_PACKAGES} gstreamer-rtp-1.0 ${RTP_VOICE_PROCESSOR_LIB})
set_target_properties(rtp PROPERTIES PREFIX "")

View File

@ -215,7 +215,12 @@ public class Dino.Plugins.Rtp.Device : MediaDevice, Object {
}
if (new_width == active_caps_width) return;
int new_height = device_caps_height * new_width / device_caps_width;
Gst.Caps new_caps = new Gst.Caps.simple("video/x-raw", "width", typeof(int), new_width, "height", typeof(int), new_height, "framerate", typeof(Gst.Fraction), device_caps_framerate_num, device_caps_framerate_den, null);
Gst.Caps new_caps;
if (device_caps_framerate_den != 0) {
new_caps = new Gst.Caps.simple("video/x-raw", "width", typeof(int), new_width, "height", typeof(int), new_height, "framerate", typeof(Gst.Fraction), device_caps_framerate_num, device_caps_framerate_den, null);
} else {
new_caps = new Gst.Caps.simple("video/x-raw", "width", typeof(int), new_width, "height", typeof(int), new_height, null);
}
double required_bitrate = get_target_bitrate(new_caps);
debug("Changing resolution width from %d to %d (requires bitrate %f, current target is %u)", active_caps_width, new_width, required_bitrate, bitrate);
if (bitrate < required_bitrate && new_width > active_caps_width) return;
@ -347,7 +352,7 @@ public class Dino.Plugins.Rtp.Device : MediaDevice, Object {
if (media == "audio") {
return Gst.Caps.from_string("audio/x-raw,rate=48000,channels=1");
} else if (media == "video" && device.caps.get_size() > 0) {
int best_index = 0;
int best_index = -1;
Value? best_fraction = null;
int best_fps = 0;
int best_width = 0;
@ -390,10 +395,25 @@ public class Dino.Plugins.Rtp.Device : MediaDevice, Object {
best_fraction = best_fraction_now;
}
}
if (best_index == -1) {
// No caps in first round, try without framerate
for (int i = 0; i < device.caps.get_size(); i++) {
unowned Gst.Structure? that = device.caps.get_structure(i);
if (!that.has_name("video/x-raw")) continue;
int width = 0, height = 0;
if (!that.has_field("width") || !that.get_int("width", out width)) continue;
if (!that.has_field("height") || !that.get_int("height", out height)) continue;
if (best_width < width || best_width == width && best_height < height) {
best_width = width;
best_height = height;
best_index = i;
}
}
}
Gst.Caps res = caps_copy_nth(device.caps, best_index);
unowned Gst.Structure? that = res.get_structure(0);
Value framerate = that.get_value("framerate");
if (framerate.type() == typeof(Gst.ValueList)) {
Value? framerate = that.get_value("framerate");
if (framerate != null && framerate.type() == typeof(Gst.ValueList) && best_fraction != null) {
that.set_value("framerate", best_fraction);
}
debug("Selected caps %s", res.to_string());

View File

@ -426,11 +426,11 @@ public class Dino.Plugins.Rtp.Plugin : RootInterface, VideoCallPlugin, Object {
if (media == "video") {
// Pick best FPS
int max_fps = 0;
int max_fps = -1;
Device? max_fps_device = null;
foreach (Device device in devices) {
int fps = get_max_fps(device);
if (fps > max_fps) {
if (fps > max_fps || max_fps_device == null) {
max_fps = fps;
max_fps_device = device;
}

Some files were not shown because too many files have changed in this diff Show More