Various call UI/UX improvements

This commit is contained in:
fiaxh 2022-02-02 21:37:05 +01:00
parent 5ed8d28a27
commit 4ef50db3e5
14 changed files with 179 additions and 117 deletions

View file

@ -96,7 +96,7 @@ public abstract interface ConversationAdditionPopulator : ConversationItemPopula
public abstract interface VideoCallPlugin : Object {
public abstract bool supports(string media);
public abstract bool supports(string? media);
// Video widget
public abstract VideoCallWidget? create_widget(WidgetType type);

View file

@ -3,6 +3,7 @@ using Gee;
using Xmpp;
public class Dino.PeerState : Object {
public signal void stream_created(string media);
public signal void counterpart_sends_video_updated(bool mute);
public signal void info_received(Xep.JingleRtp.CallSessionInfo session_info);
@ -214,14 +215,14 @@ public class Dino.PeerState : Object {
// If video_content_parameter == null && !mute we're trying to mute a non-existant feed. It will be muted as soon as it is created.
}
public Xep.JingleRtp.Stream? get_video_stream(Call call) {
public Xep.JingleRtp.Stream? get_video_stream() {
if (video_content_parameter != null) {
return video_content_parameter.stream;
}
return null;
}
public Xep.JingleRtp.Stream? get_audio_stream(Call call) {
public Xep.JingleRtp.Stream? get_audio_stream() {
if (audio_content_parameter != null) {
return audio_content_parameter.stream;
}
@ -235,8 +236,8 @@ public class Dino.PeerState : Object {
session.terminated.connect((stream, we_terminated, reason_name, reason_text) =>
session_terminated(we_terminated, reason_name, reason_text)
);
session.additional_content_add_incoming.connect((session,stream, content) =>
on_incoming_content_add(stream, session, content)
session.additional_content_add_incoming.connect((stream, content) =>
on_incoming_content_add(stream, content.session, content)
);
foreach (Xep.Jingle.Content content in session.contents) {
@ -358,6 +359,8 @@ public class Dino.PeerState : Object {
} else if (media == "audio" && !we_should_send_audio) {
mute_own_audio(true);
}
stream_created(media);
}
private void on_counterpart_mute_update(bool mute, string? media) {

View file

@ -9,6 +9,7 @@ public class Dino.CallState : Object {
public signal void peer_left(Jid jid, PeerState peer_state, string? reason_name, string? reason_text);
public StreamInteractor stream_interactor;
public Plugins.VideoCallPlugin call_plugin = Dino.Application.get_default().plugin_registry.video_call_plugin;
public Call call;
public Xep.Muji.GroupCall? group_call { get; set; }
public Jid? parent_muc { get; set; }
@ -109,22 +110,25 @@ public class Dino.CallState : Object {
terminated(call.account.bare_jid, null, null);
}
public void end() {
public void end(string? reason_text = null) {
var peers_cpy = new ArrayList<PeerState>();
peers_cpy.add_all(peers.values);
if (group_call != null) {
stream_interactor.get_module(MucManager.IDENTITY).part(call.account, group_call.muc_jid);
XmppStream stream = stream_interactor.get_stream(call.account);
if (stream != null) {
stream.get_module(Xep.Muc.Module.IDENTITY).exit(stream, group_call.muc_jid);
}
}
if (call.state == Call.State.IN_PROGRESS || call.state == Call.State.ESTABLISHING) {
foreach (PeerState peer in peers_cpy) {
peer.end(Xep.Jingle.ReasonElement.SUCCESS);
peer.end(Xep.Jingle.ReasonElement.SUCCESS, reason_text);
}
call.state = Call.State.ENDED;
} else if (call.state == Call.State.RINGING) {
foreach (PeerState peer in peers_cpy) {
peer.end(Xep.Jingle.ReasonElement.CANCEL);
peer.end(Xep.Jingle.ReasonElement.CANCEL, reason_text);
}
if (parent_muc != null && group_call != null) {
XmppStream stream = stream_interactor.get_stream(call.account);
@ -138,7 +142,7 @@ public class Dino.CallState : Object {
call.end_time = new DateTime.now_utc();
terminated(call.account.bare_jid, null, null);
terminated(call.account.bare_jid, null, reason_text);
}
public void mute_own_audio(bool mute) {
@ -168,7 +172,7 @@ public class Dino.CallState : Object {
debug("[%s] Inviting to muji call %s", call.account.bare_jid.to_string(), invitee.to_string());
yield stream.get_module(Xep.Muc.Module.IDENTITY).change_affiliation(stream, group_call.muc_jid, invitee, null, "owner");
stream.get_module(Xep.MujiMeta.Module.IDENTITY).send_invite(stream, invitee, group_call.muc_jid, we_should_send_video);
stream.get_module(Xep.MujiMeta.Module.IDENTITY).send_invite(stream, invitee, group_call.muc_jid, we_should_send_video, message_type);
// If the peer hasn't accepted within a minute, retract the invite
Timeout.add_seconds(60, () => {
@ -183,13 +187,43 @@ public class Dino.CallState : Object {
if (!contains_peer) {
debug("[%s] Retracting invite to %s from %s", call.account.bare_jid.to_string(), group_call.muc_jid.to_string(), invitee.to_string());
stream.get_module(Xep.MujiMeta.Module.IDENTITY).send_invite_retract_to_peer(stream, invitee, group_call.muc_jid);
stream.get_module(Xep.MujiMeta.Module.IDENTITY).send_invite_retract_to_peer(stream, invitee, group_call.muc_jid, message_type);
stream.get_module(Xep.Muc.Module.IDENTITY).change_affiliation.begin(stream, group_call.muc_jid, invitee, null, "none");
}
return false;
});
}
public Plugins.MediaDevice? get_microphone_device() {
if (peers.is_empty) return null;
var audio_stream = peers.values.to_array()[0].get_audio_stream();
return call_plugin.get_device(audio_stream, false);
}
public Plugins.MediaDevice? get_speaker_device() {
if (peers.is_empty) return null;
var audio_stream = peers.values.to_array()[0].get_audio_stream();
return call_plugin.get_device(audio_stream, true);
}
public Plugins.MediaDevice? get_video_device() {
if (peers.is_empty) return null;
var video_stream = peers.values.to_array()[0].get_video_stream();
return call_plugin.get_device(video_stream, false);
}
public void set_audio_device(Plugins.MediaDevice? device) {
foreach (PeerState peer_state in peers.values) {
call_plugin.set_device(peer_state.get_audio_stream(), device);
}
}
public void set_video_device(Plugins.MediaDevice? device) {
foreach (PeerState peer_state in peers.values) {
call_plugin.set_device(peer_state.get_video_stream(), device);
}
}
internal void rename_peer(Jid from_jid, Jid to_jid) {
debug("[%s] Renaming %s to %s exists %s", call.account.bare_jid.to_string(), from_jid.to_string(), to_jid.to_string(), peers.has_key(from_jid).to_string());
PeerState? peer_state = peers[from_jid];
@ -226,23 +260,35 @@ public class Dino.CallState : Object {
this.bind_property("group-call", peer_state, "group-call", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL);
peer_state.session_terminated.connect((we_terminated, reason_name, reason_text) => {
debug("[%s] Peer left %s: %s %s (%i peers remaining)", call.account.bare_jid.to_string(), reason_text ?? "", reason_name ?? "", peer_state.jid.to_string(), peers.size);
peers.unset(peer_state.jid);
debug("[%s] Peer left %s left %i", call.account.bare_jid.to_string(), peer_state.jid.to_string(), peers.size);
if (peers.is_empty) {
if (group_call != null) group_call.leave(stream_interactor.get_stream(call.account));
on_call_terminated(peer_state.jid, we_terminated, reason_name, reason_text);
if (group_call != null) {
group_call.leave(stream_interactor.get_stream(call.account));
on_call_terminated(peer_state.jid, we_terminated, null, "All participants have left the group call");
} else {
on_call_terminated(peer_state.jid, we_terminated, reason_name, reason_text);
}
} else {
peer_left(peer_state.jid, peer_state, reason_name, reason_text);
}
});
}
public async bool can_convert_into_groupcall() {
if (peers.size == 0) return false;
Jid peer = peers.keys.to_array()[0];
bool peer_has_feature = yield stream_interactor.get_module(EntityInfo.IDENTITY).has_feature(call.account, peer, Xep.Muji.NS_URI);
bool can_initiate = stream_interactor.get_module(Calls.IDENTITY).can_initiate_groupcall(call.account);
return peer_has_feature && can_initiate;
}
public async void convert_into_group_call() {
XmppStream stream = stream_interactor.get_stream(call.account);
if (stream == null) return;
Jid? muc_jid = null;
Jid? muc_jid = stream_interactor.get_module(MucManager.IDENTITY).default_muc_server[call.account];
if (muc_jid == null) {
warning("Failed to initiate group call: MUC server not known.");
return;
@ -320,11 +366,18 @@ public class Dino.CallState : Object {
this.group_call.peer_left.connect((jid) => {
debug("[%s] Group call peer left: %s", call.account.bare_jid.to_string(), jid.to_string());
PeerState? peer_state = peers[jid];
if (!peers.has_key(jid)) return;
// end() will in the end cause a `peer_left` signal and removal from `peers`
peers[jid].end(Xep.Jingle.ReasonElement.CANCEL, "Peer left the MUJI MUC");
peer_left(jid, peer_state, Xep.Jingle.ReasonElement.CANCEL, "Peer left the MUJI MUC");
peer_state.end(Xep.Jingle.ReasonElement.CANCEL, "Peer left the MUJI MUC");
peers.unset(jid);
});
if (group_call.peers_to_connect_to.size > 3) {
end("Call too full - P2p calls don't work well with many participants");
return;
}
// Call all peers that are in the room already
foreach (Jid peer_jid in group_call.peers_to_connect_to) {
// Don't establish connection if we have one already (the person that invited us to the call)

View file

@ -67,38 +67,24 @@ namespace Dino {
return call_state;
}
public async bool can_do_audio_calls_async(Conversation conversation) {
if (!can_do_audio_calls()) return false;
if (conversation.type_ == Conversation.Type.CHAT) {
return (yield get_call_resources(conversation.account, conversation.counterpart)).size > 0 || has_jmi_resources(conversation.counterpart);
} else {
return stream_interactor.get_module(MucManager.IDENTITY).is_private_room(conversation.account, conversation.counterpart);
}
}
private bool can_do_audio_calls() {
public bool can_we_do_calls(Account account) {
Plugins.VideoCallPlugin? plugin = Application.get_default().plugin_registry.video_call_plugin;
if (plugin == null) return false;
return plugin.supports("audio");
return plugin.supports(null);
}
public async bool can_do_video_calls_async(Conversation conversation) {
if (!can_do_video_calls()) return false;
public async bool can_conversation_do_calls(Conversation conversation) {
if (conversation.type_ == Conversation.Type.CHAT) {
return (yield get_call_resources(conversation.account, conversation.counterpart)).size > 0 || has_jmi_resources(conversation.counterpart);
} else {
return stream_interactor.get_module(MucManager.IDENTITY).is_private_room(conversation.account, conversation.counterpart);
bool is_private = stream_interactor.get_module(MucManager.IDENTITY).is_private_room(conversation.account, conversation.counterpart);
return is_private && can_initiate_groupcall(conversation.account);
}
}
private bool can_do_video_calls() {
Plugins.VideoCallPlugin? plugin = Application.get_default().plugin_registry.video_call_plugin;
if (plugin == null) return false;
return plugin.supports("video");
public bool can_initiate_groupcall(Account account) {
return stream_interactor.get_module(MucManager.IDENTITY).default_muc_server[account] != null;
}
public async Gee.List<Jid> get_call_resources(Account account, Jid counterpart) {
@ -107,7 +93,10 @@ namespace Dino {
XmppStream? stream = stream_interactor.get_stream(account);
if (stream == null) return ret;
Gee.List<Jid>? full_jids = stream.get_flag(Presence.Flag.IDENTITY).get_resources(counterpart);
Presence.Flag? presence_flag = stream.get_flag(Presence.Flag.IDENTITY);
if (presence_flag == null) return ret;
Gee.List<Jid>? full_jids = presence_flag.get_resources(counterpart);
if (full_jids == null) return ret;
foreach (Jid full_jid in full_jids) {
@ -148,11 +137,6 @@ namespace Dino {
}
private void on_incoming_call(Account account, Xep.Jingle.Session session) {
if (!can_do_audio_calls()) {
warning("Incoming call but no call support detected. Ignoring.");
return;
}
Jid? muji_muc = null;
bool counterpart_wants_video = false;
foreach (Xep.Jingle.Content content in session.contents) {
@ -268,7 +252,7 @@ namespace Dino {
if (!call.account.equals(account)) return;
// We already know the call; this is a reflection of our own invite
if (call_states[call].parent_muc.equals_bare(inviter_jid)) return;
if (call_states[call].parent_muc != null && call_states[call].parent_muc.equals_bare(inviter_jid)) return;
if (call.counterparts.contains(inviter_jid) && call_states[call].accepted) {
// A call is converted into a group call.
@ -337,11 +321,6 @@ namespace Dino {
Xep.JingleMessageInitiation.Module mi_module = stream_interactor.module_manager.get_module(account, Xep.JingleMessageInitiation.Module.IDENTITY);
mi_module.session_proposed.connect((from, to, sid, descriptions) => {
if (!can_do_audio_calls()) {
warning("Incoming call but no call support detected. Ignoring.");
return;
}
bool audio_requested = descriptions.any_match((description) => description.ns_uri == Xep.JingleRtp.NS_URI && description.get_attribute("media") == "audio");
bool video_requested = descriptions.any_match((description) => description.ns_uri == Xep.JingleRtp.NS_URI && description.get_attribute("media") == "video");
if (!audio_requested && !video_requested) return;

View file

@ -118,6 +118,7 @@ public class NotificationEvents : StreamInteractionModule, Object {
}
private async void on_call_incoming(Call call, CallState call_state, Conversation conversation, bool video) {
if (!stream_interactor.get_module(Calls.IDENTITY).can_we_do_calls(call.account)) return;
string conversation_display_name = get_conversation_display_name(stream_interactor, conversation, null);
NotificationProvider notifier = yield notifier.wait_async();

View file

@ -20,7 +20,7 @@ namespace Dino.Ui {
public Revealer header_bar_revealer = new Revealer() { halign=Align.END, valign=Align.START, transition_type=RevealerTransitionType.CROSSFADE, transition_duration=200, visible=true };
public Box own_video_box = new Box(Orientation.HORIZONTAL, 0) { halign=Align.END, valign=Align.END, visible=true };
public Revealer invite_button_revealer = new Revealer() { margin_top=50, margin_right=30, halign=Align.END, valign=Align.START, transition_type=RevealerTransitionType.CROSSFADE, transition_duration=200 };
public Button invite_button = new Button.from_icon_name("dino-account-plus") { relief=ReliefStyle.NONE, visible=false };
public Button invite_button = new Button.from_icon_name("dino-account-plus") { relief=ReliefStyle.NONE, visible=true };
private Widget? own_video = null;
private HashMap<string, ParticipantWidget> participant_widgets = new HashMap<string, ParticipantWidget>();
private ArrayList<string> participants = new ArrayList<string>();
@ -191,7 +191,11 @@ namespace Dino.Ui {
} else if (reason_name == Xmpp.Xep.Jingle.ReasonElement.DECLINE || reason_name == Xmpp.Xep.Jingle.ReasonElement.BUSY) {
text = _("%s declined the call").printf(who_terminated);
} else {
text = "The call has been terminated: " + (reason_name ?? "") + " " + (reason_text ?? "");
if (reason_text == null) {
text = "The call has been terminated" + " " + (reason_name ?? "");
} else {
text = reason_text + " " + (reason_name ?? "");
}
}
bottom_bar.show_counterpart_ended(text);

View file

@ -134,16 +134,23 @@ public class Dino.Ui.CallWindowController : Object {
Jid peer_jid = peer_state.jid;
peer_states[peer_id] = peer_state;
peer_state.stream_created.connect((media) => {
if (media == "audio") {
update_audio_device_choices();
} else if (media == "video") {
update_video_device_choices();
}
});
peer_state.connection_ready.connect(() => {
call_window.set_status(peer_state.internal_id, "");
if (participant_widgets.size == 1) {
// This is the first peer.
// If it can do MUJI, show invite button.
call_window.invite_button_revealer.visible = true;
// stream_interactor.get_module(EntityInfo.IDENTITY).has_feature.begin(call.account, peer_state.jid, Xep.Muji.NS_URI, (_, res) => {
// bool has_feature = stream_interactor.get_module(EntityInfo.IDENTITY).has_feature.end(res);
// call_window.invite_button_revealer.visible = has_feature;
// });
call_state.can_convert_into_groupcall.begin((_, res) => {
bool can_convert = call_state.can_convert_into_groupcall.end(res);
call_window.invite_button_revealer.visible = can_convert;
});
call_plugin.devices_changed.connect((media, incoming) => {
if (media == "audio") update_audio_device_choices();
@ -165,7 +172,7 @@ public class Dino.Ui.CallWindowController : Object {
if (!(participant_videos[peer_id] is Widget)) return;
Widget widget = (Widget) participant_videos[peer_id];
call_window.set_video(peer_id, widget);
participant_videos[peer_id].display_stream(peer_state.get_video_stream(call), peer_jid);
participant_videos[peer_id].display_stream(peer_state.get_video_stream(), peer_jid);
}
});
peer_state.info_received.connect((session_info) => {
@ -264,7 +271,7 @@ public class Dino.Ui.CallWindowController : Object {
private void update_audio_device_choices() {
if (call_plugin.get_devices("audio", true).size == 0 || call_plugin.get_devices("audio", false).size == 0) {
call_window.bottom_bar.show_audio_device_error();
} /*else if (call_plugin.get_devices("audio", true).size == 1 && call_plugin.get_devices("audio", false).size == 1) {
} else if (call_plugin.get_devices("audio", true).size == 1 && call_plugin.get_devices("audio", false).size == 1) {
call_window.bottom_bar.show_audio_device_choices(false);
return;
}
@ -273,34 +280,31 @@ public class Dino.Ui.CallWindowController : Object {
update_current_audio_device(audio_settings_popover);
audio_settings_popover.microphone_selected.connect((device) => {
call_plugin.set_device(calls.get_audio_stream(call), device);
call_state.set_audio_device(device);
update_current_audio_device(audio_settings_popover);
});
audio_settings_popover.speaker_selected.connect((device) => {
call_plugin.set_device(calls.get_audio_stream(call), device);
call_state.set_audio_device(device);
update_current_audio_device(audio_settings_popover);
});
calls.stream_created.connect((call, media) => {
if (media == "audio") {
update_current_audio_device(audio_settings_popover);
}
});*/
// calls.stream_created.connect((call, media) => {
// if (media == "audio") {
// update_current_audio_device(audio_settings_popover);
// }
// });
}
/*private void update_current_audio_device(AudioSettingsPopover audio_settings_popover) {
Xmpp.Xep.JingleRtp.Stream stream = calls.get_audio_stream(call);
if (stream != null) {
audio_settings_popover.current_microphone_device = call_plugin.get_device(stream, false);
audio_settings_popover.current_speaker_device = call_plugin.get_device(stream, true);
}
}*/
private void update_current_audio_device(AudioSettingsPopover audio_settings_popover) {
audio_settings_popover.current_microphone_device = call_state.get_microphone_device();
audio_settings_popover.current_speaker_device = call_state.get_speaker_device();
}
private void update_video_device_choices() {
int device_count = call_plugin.get_devices("video", false).size;
if (device_count == 0) {
call_window.bottom_bar.show_video_device_error();
} /*else if (device_count == 1 || calls.get_video_stream(call) == null) {
} else if (device_count == 1 || call_state.get_video_device() == null) {
call_window.bottom_bar.show_video_device_choices(false);
return;
}
@ -309,23 +313,20 @@ public class Dino.Ui.CallWindowController : Object {
update_current_video_device(video_settings_popover);
video_settings_popover.camera_selected.connect((device) => {
call_plugin.set_device(calls.get_video_stream(call), device);
call_state.set_video_device(device);
update_current_video_device(video_settings_popover);
own_video.display_device(device);
});
calls.stream_created.connect((call, media) => {
if (media == "video") {
update_current_video_device(video_settings_popover);
}
});*/
// call_state.stream_created.connect((call, media) => {
// if (media == "video") {
// update_current_video_device(video_settings_popover);
// }
// });
}
/*private void update_current_video_device(VideoSettingsPopover video_settings_popover) {
Xmpp.Xep.JingleRtp.Stream stream = calls.get_video_stream(call);
if (stream != null) {
video_settings_popover.current_device = call_plugin.get_device(stream, false);
}
}*/
private void update_current_video_device(VideoSettingsPopover video_settings_popover) {
video_settings_popover.current_device = call_state.get_video_device();
}
private void update_own_video() {
if (this.call_window.bottom_bar.video_enabled) {

View file

@ -111,17 +111,30 @@ namespace Dino.Ui {
incoming_call_revealer.get_style_context().remove_class("incoming");
outer_additional_box.get_style_context().remove_class("incoming-call-box");
switch (call.state) {
// It doesn't make sense to display MUC calls as missed or declined by the whole MUC. Just display as ended.
// TODO: maybe not let them be missed/declined in first place.
Call.State relevant_state = call.state;
if (conversation.type_ == Conversation.Type.GROUPCHAT && call.direction == Call.DIRECTION_OUTGOING &&
(relevant_state == Call.State.MISSED || relevant_state == Call.State.DECLINED)) {
relevant_state = Call.State.ENDED;
}
switch (relevant_state) {
case Call.State.RINGING:
image.set_from_icon_name("dino-phone-ring-symbolic", IconSize.LARGE_TOOLBAR);
if (call.direction == Call.DIRECTION_INCOMING) {
bool video = call_manager.should_we_send_video();
title_label.label = video ? _("Incoming video call") : _("Incoming call");
subtitle_label.label = "Ring ring…!";
incoming_call_box.visible = true;
incoming_call_revealer.reveal_child = true;
incoming_call_revealer.get_style_context().add_class("incoming");
outer_additional_box.get_style_context().add_class("incoming-call-box");
if (stream_interactor.get_module(Calls.IDENTITY).can_we_do_calls(call.account)) {
subtitle_label.label = "Ring ring…!";
incoming_call_box.visible = true;
incoming_call_revealer.reveal_child = true;
incoming_call_revealer.get_style_context().add_class("incoming");
outer_additional_box.get_style_context().add_class("incoming-call-box");
} else {
subtitle_label.label = "Dependencies for call support not met";
}
} else {
title_label.label = _("Calling…");
subtitle_label.label = "Ring ring…?";

View file

@ -115,17 +115,11 @@ namespace Dino.Ui {
return;
}
if (conversation.type_ == Conversation.Type.CHAT) {
Conversation conv_bak = conversation;
bool audio_works = yield stream_interactor.get_module(Calls.IDENTITY).can_do_audio_calls_async(conversation);
bool video_works = yield stream_interactor.get_module(Calls.IDENTITY).can_do_video_calls_async(conversation);
if (conv_bak != conversation) return;
Conversation conv_bak = conversation;
bool can_do_calls = yield stream_interactor.get_module(Calls.IDENTITY).can_conversation_do_calls(conversation);
if (conv_bak != conversation) return;
visible = audio_works;
video_button.visible = video_works;
} else {
visible = false;
}
visible = video_button.visible = can_do_calls;
}
public new void unset_conversation() { }

View file

@ -101,11 +101,18 @@ namespace Dino.Plugins.Omemo.DtlsSrtpVerificationDraft {
if (fingerprint_node == null) continue;
string fingerprint = fingerprint_node.get_deep_string_content();
Xep.Omemo.OmemoEncryptor encryptor = stream.get_module(Xep.Omemo.OmemoEncryptor.IDENTITY);
Xep.Omemo.EncryptionData enc_data = encryptor.encrypt_plaintext(fingerprint);
encryptor.encrypt_key(enc_data, iq.to.bare_jid, device_id_by_jingle_sid[sid]);
StanzaNode? encrypted_node = null;
try {
Xep.Omemo.OmemoEncryptor encryptor = stream.get_module(Xep.Omemo.OmemoEncryptor.IDENTITY);
Xep.Omemo.EncryptionData enc_data = encryptor.encrypt_plaintext(fingerprint);
encryptor.encrypt_key(enc_data, iq.to.bare_jid, device_id_by_jingle_sid[sid]);
encrypted_node = enc_data.get_encrypted_node();
} catch (Error e) {
warning("Error while OMEMO-encrypting call keys: %s", e.message);
return;
}
StanzaNode new_fingerprint_node = new StanzaNode.build("fingerprint", NS_URI).add_self_xmlns().put_node(enc_data.get_encrypted_node());
StanzaNode new_fingerprint_node = new StanzaNode.build("fingerprint", NS_URI).add_self_xmlns().put_node(encrypted_node);
string? hash_attr = fingerprint_node.get_attribute("hash", Xep.JingleIceUdp.DTLS_NS_URI);
string? setup_attr = fingerprint_node.get_attribute("setup", Xep.JingleIceUdp.DTLS_NS_URI);
if (hash_attr != null) new_fingerprint_node.put_attribute("hash", hash_attr);

View file

@ -285,7 +285,7 @@ public class Dino.Plugins.Rtp.Plugin : RootInterface, VideoCallPlugin, Object {
Gst.deinit();
}
public bool supports(string media) {
public bool supports(string? media) {
if (!codec_util.is_element_supported("rtpbin")) return false;
if (media == "audio") {
@ -310,6 +310,7 @@ public class Dino.Plugins.Rtp.Plugin : RootInterface, VideoCallPlugin, Object {
}
public Gee.List<MediaDevice> get_devices(string media, bool incoming) {
if (media == "video" && !incoming) {
return get_video_sources();
}

View file

@ -218,7 +218,10 @@ public class Module : XmppStreamModule {
public async void change_affiliation(XmppStream stream, Jid muc_jid, Jid? user_jid, string? nick, string new_affiliation) {
StanzaNode item_node = new StanzaNode.build("item", NS_URI_ADMIN)
.put_attribute("affiliation", new_affiliation, NS_URI_ADMIN);
if (user_jid != null) item_node.put_attribute("jid", user_jid.to_string(), NS_URI_ADMIN);
if (user_jid != null) {
// Some servers don't allow full JIDs and reply error:modify - jid-malformed - "Bare JID expected, got full JID". Make them bare JIDs.
item_node.put_attribute("jid", user_jid.bare_jid.to_string(), NS_URI_ADMIN);
}
if (nick != null) item_node.put_attribute("nick", nick, NS_URI_ADMIN);
StanzaNode query = new StanzaNode.build("query", NS_URI_ADMIN).add_self_xmlns().put_node(item_node);

View file

@ -237,11 +237,14 @@ namespace Xmpp.Xep.Muji {
public override void attach(XmppStream stream) {
stream.add_flag(new Flag());
stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI);
stream.get_module(Presence.Module.IDENTITY).received_available.connect(on_received_available);
stream.get_module(Presence.Module.IDENTITY).received_unavailable.connect(on_received_unavailable);
}
public override void detach(XmppStream stream) { }
public override void detach(XmppStream stream) {
stream.get_module(ServiceDiscovery.Module.IDENTITY).remove_feature(stream, NS_URI);
}
public override string get_ns() {
return NS_URI;

View file

@ -11,7 +11,7 @@ namespace Xmpp.Xep.MujiMeta {
public signal void call_accepted(Jid from, Jid muc_jid, string message_type);
public signal void call_rejected(Jid from, Jid to, Jid muc_jid, string message_type);
public void send_invite(XmppStream stream, Jid invitee, Jid muc_jid, bool video, string? message_type = null) {
public void send_invite(XmppStream stream, Jid invitee, Jid muc_jid, bool video, string message_type) {
var invite_node = new StanzaNode.build("propose", NS_URI).put_attribute("muc", muc_jid.to_string());
invite_node.put_node(new StanzaNode.build("description", Xep.JingleRtp.NS_URI).add_self_xmlns().put_attribute("media", "audio"));
if (video) {
@ -23,27 +23,27 @@ namespace Xmpp.Xep.MujiMeta {
stream.get_module(MessageModule.IDENTITY).send_message.begin(stream, invite_message);
}
public void send_invite_retract_to_peer(XmppStream stream, Jid invitee, Jid muc_jid, string? message_type = null) {
public void send_invite_retract_to_peer(XmppStream stream, Jid invitee, Jid muc_jid, string message_type) {
send_jmi_message(stream, "retract", invitee, muc_jid, message_type);
}
public void send_invite_accept_to_peer(XmppStream stream, Jid invitor, Jid muc_jid, string? message_type = null) {
public void send_invite_accept_to_peer(XmppStream stream, Jid invitor, Jid muc_jid, string message_type) {
send_jmi_message(stream, "accept", invitor, muc_jid, message_type);
}
public void send_invite_accept_to_self(XmppStream stream, Jid muc_jid) {
send_jmi_message(stream, "accept", Bind.Flag.get_my_jid(stream).bare_jid, muc_jid);
send_jmi_message(stream, "accept", Bind.Flag.get_my_jid(stream).bare_jid, muc_jid, MessageStanza.TYPE_CHAT);
}
public void send_invite_reject_to_peer(XmppStream stream, Jid invitor, Jid muc_jid, string? message_type = null) {
public void send_invite_reject_to_peer(XmppStream stream, Jid invitor, Jid muc_jid, string message_type) {
send_jmi_message(stream, "reject", invitor, muc_jid, message_type);
}
public void send_invite_reject_to_self(XmppStream stream, Jid muc_jid) {
send_jmi_message(stream, "reject", Bind.Flag.get_my_jid(stream).bare_jid, muc_jid);
send_jmi_message(stream, "reject", Bind.Flag.get_my_jid(stream).bare_jid, muc_jid, MessageStanza.TYPE_CHAT);
}
private void send_jmi_message(XmppStream stream, string name, Jid to, Jid muc, string? message_type = null) {
private void send_jmi_message(XmppStream stream, string name, Jid to, Jid muc, string message_type) {
var jmi_node = new StanzaNode.build(name, NS_URI).add_self_xmlns().put_attribute("muc", muc.to_string());
var muji_node = new StanzaNode.build("muji", NS_URI).add_self_xmlns().put_node(jmi_node);