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 interface VideoCallPlugin : Object {
public abstract bool supports(string media); public abstract bool supports(string? media);
// Video widget // Video widget
public abstract VideoCallWidget? create_widget(WidgetType type); public abstract VideoCallWidget? create_widget(WidgetType type);

View file

@ -3,6 +3,7 @@ using Gee;
using Xmpp; using Xmpp;
public class Dino.PeerState : Object { public class Dino.PeerState : Object {
public signal void stream_created(string media);
public signal void counterpart_sends_video_updated(bool mute); public signal void counterpart_sends_video_updated(bool mute);
public signal void info_received(Xep.JingleRtp.CallSessionInfo session_info); 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. // 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) { if (video_content_parameter != null) {
return video_content_parameter.stream; return video_content_parameter.stream;
} }
return null; return null;
} }
public Xep.JingleRtp.Stream? get_audio_stream(Call call) { public Xep.JingleRtp.Stream? get_audio_stream() {
if (audio_content_parameter != null) { if (audio_content_parameter != null) {
return audio_content_parameter.stream; 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.connect((stream, we_terminated, reason_name, reason_text) =>
session_terminated(we_terminated, reason_name, reason_text) session_terminated(we_terminated, reason_name, reason_text)
); );
session.additional_content_add_incoming.connect((session,stream, content) => session.additional_content_add_incoming.connect((stream, content) =>
on_incoming_content_add(stream, session, content) on_incoming_content_add(stream, content.session, content)
); );
foreach (Xep.Jingle.Content content in session.contents) { 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) { } else if (media == "audio" && !we_should_send_audio) {
mute_own_audio(true); mute_own_audio(true);
} }
stream_created(media);
} }
private void on_counterpart_mute_update(bool mute, string? 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 signal void peer_left(Jid jid, PeerState peer_state, string? reason_name, string? reason_text);
public StreamInteractor stream_interactor; public StreamInteractor stream_interactor;
public Plugins.VideoCallPlugin call_plugin = Dino.Application.get_default().plugin_registry.video_call_plugin;
public Call call; public Call call;
public Xep.Muji.GroupCall? group_call { get; set; } public Xep.Muji.GroupCall? group_call { get; set; }
public Jid? parent_muc { get; set; } public Jid? parent_muc { get; set; }
@ -109,22 +110,25 @@ public class Dino.CallState : Object {
terminated(call.account.bare_jid, null, null); terminated(call.account.bare_jid, null, null);
} }
public void end() { public void end(string? reason_text = null) {
var peers_cpy = new ArrayList<PeerState>(); var peers_cpy = new ArrayList<PeerState>();
peers_cpy.add_all(peers.values); peers_cpy.add_all(peers.values);
if (group_call != null) { 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) { if (call.state == Call.State.IN_PROGRESS || call.state == Call.State.ESTABLISHING) {
foreach (PeerState peer in peers_cpy) { 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; call.state = Call.State.ENDED;
} else if (call.state == Call.State.RINGING) { } else if (call.state == Call.State.RINGING) {
foreach (PeerState peer in peers_cpy) { 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) { if (parent_muc != null && group_call != null) {
XmppStream stream = stream_interactor.get_stream(call.account); XmppStream stream = stream_interactor.get_stream(call.account);
@ -138,7 +142,7 @@ public class Dino.CallState : Object {
call.end_time = new DateTime.now_utc(); 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) { 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()); 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"); 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 // If the peer hasn't accepted within a minute, retract the invite
Timeout.add_seconds(60, () => { Timeout.add_seconds(60, () => {
@ -183,13 +187,43 @@ public class Dino.CallState : Object {
if (!contains_peer) { 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()); 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"); stream.get_module(Xep.Muc.Module.IDENTITY).change_affiliation.begin(stream, group_call.muc_jid, invitee, null, "none");
} }
return false; 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) { 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()); 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]; 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); 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) => { 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); 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 (peers.is_empty) {
if (group_call != null) group_call.leave(stream_interactor.get_stream(call.account)); 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); on_call_terminated(peer_state.jid, we_terminated, reason_name, reason_text);
}
} else { } else {
peer_left(peer_state.jid, peer_state, reason_name, reason_text); 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() { public async void convert_into_group_call() {
XmppStream stream = stream_interactor.get_stream(call.account); XmppStream stream = stream_interactor.get_stream(call.account);
if (stream == null) return; 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) { if (muc_jid == null) {
warning("Failed to initiate group call: MUC server not known."); warning("Failed to initiate group call: MUC server not known.");
return; return;
@ -320,11 +366,18 @@ public class Dino.CallState : Object {
this.group_call.peer_left.connect((jid) => { this.group_call.peer_left.connect((jid) => {
debug("[%s] Group call peer left: %s", call.account.bare_jid.to_string(), jid.to_string()); 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; if (!peers.has_key(jid)) return;
// end() will in the end cause a `peer_left` signal and removal from `peers` peer_left(jid, peer_state, Xep.Jingle.ReasonElement.CANCEL, "Peer left the MUJI MUC");
peers[jid].end(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 // Call all peers that are in the room already
foreach (Jid peer_jid in group_call.peers_to_connect_to) { 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) // 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; return call_state;
} }
public async bool can_do_audio_calls_async(Conversation conversation) { public bool can_we_do_calls(Account account) {
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() {
Plugins.VideoCallPlugin? plugin = Application.get_default().plugin_registry.video_call_plugin; Plugins.VideoCallPlugin? plugin = Application.get_default().plugin_registry.video_call_plugin;
if (plugin == null) return false; if (plugin == null) return false;
return plugin.supports("audio"); return plugin.supports(null);
} }
public async bool can_do_video_calls_async(Conversation conversation) { public async bool can_conversation_do_calls(Conversation conversation) {
if (!can_do_video_calls()) return false;
if (conversation.type_ == Conversation.Type.CHAT) { if (conversation.type_ == Conversation.Type.CHAT) {
return (yield get_call_resources(conversation.account, conversation.counterpart)).size > 0 || has_jmi_resources(conversation.counterpart); return (yield get_call_resources(conversation.account, conversation.counterpart)).size > 0 || has_jmi_resources(conversation.counterpart);
} else { } 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() { public bool can_initiate_groupcall(Account account) {
Plugins.VideoCallPlugin? plugin = Application.get_default().plugin_registry.video_call_plugin; return stream_interactor.get_module(MucManager.IDENTITY).default_muc_server[account] != null;
if (plugin == null) return false;
return plugin.supports("video");
} }
public async Gee.List<Jid> get_call_resources(Account account, Jid counterpart) { 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); XmppStream? stream = stream_interactor.get_stream(account);
if (stream == null) return ret; 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; if (full_jids == null) return ret;
foreach (Jid full_jid in full_jids) { foreach (Jid full_jid in full_jids) {
@ -148,11 +137,6 @@ namespace Dino {
} }
private void on_incoming_call(Account account, Xep.Jingle.Session session) { 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; Jid? muji_muc = null;
bool counterpart_wants_video = false; bool counterpart_wants_video = false;
foreach (Xep.Jingle.Content content in session.contents) { foreach (Xep.Jingle.Content content in session.contents) {
@ -268,7 +252,7 @@ namespace Dino {
if (!call.account.equals(account)) return; if (!call.account.equals(account)) return;
// We already know the call; this is a reflection of our own invite // 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) { if (call.counterparts.contains(inviter_jid) && call_states[call].accepted) {
// A call is converted into a group call. // 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); 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) => { 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 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"); 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; 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) { 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); string conversation_display_name = get_conversation_display_name(stream_interactor, conversation, null);
NotificationProvider notifier = yield notifier.wait_async(); 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 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 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 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 Widget? own_video = null;
private HashMap<string, ParticipantWidget> participant_widgets = new HashMap<string, ParticipantWidget>(); private HashMap<string, ParticipantWidget> participant_widgets = new HashMap<string, ParticipantWidget>();
private ArrayList<string> participants = new ArrayList<string>(); 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) { } else if (reason_name == Xmpp.Xep.Jingle.ReasonElement.DECLINE || reason_name == Xmpp.Xep.Jingle.ReasonElement.BUSY) {
text = _("%s declined the call").printf(who_terminated); text = _("%s declined the call").printf(who_terminated);
} else { } 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); bottom_bar.show_counterpart_ended(text);

View file

@ -134,16 +134,23 @@ public class Dino.Ui.CallWindowController : Object {
Jid peer_jid = peer_state.jid; Jid peer_jid = peer_state.jid;
peer_states[peer_id] = peer_state; 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(() => { peer_state.connection_ready.connect(() => {
call_window.set_status(peer_state.internal_id, ""); call_window.set_status(peer_state.internal_id, "");
if (participant_widgets.size == 1) { if (participant_widgets.size == 1) {
// This is the first peer. // This is the first peer.
// If it can do MUJI, show invite button. // 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) => { call_state.can_convert_into_groupcall.begin((_, res) => {
// bool has_feature = stream_interactor.get_module(EntityInfo.IDENTITY).has_feature.end(res); bool can_convert = call_state.can_convert_into_groupcall.end(res);
// call_window.invite_button_revealer.visible = has_feature; call_window.invite_button_revealer.visible = can_convert;
// }); });
call_plugin.devices_changed.connect((media, incoming) => { call_plugin.devices_changed.connect((media, incoming) => {
if (media == "audio") update_audio_device_choices(); 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; if (!(participant_videos[peer_id] is Widget)) return;
Widget widget = (Widget) participant_videos[peer_id]; Widget widget = (Widget) participant_videos[peer_id];
call_window.set_video(peer_id, widget); 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) => { peer_state.info_received.connect((session_info) => {
@ -264,7 +271,7 @@ public class Dino.Ui.CallWindowController : Object {
private void update_audio_device_choices() { private void update_audio_device_choices() {
if (call_plugin.get_devices("audio", true).size == 0 || call_plugin.get_devices("audio", false).size == 0) { 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(); 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); call_window.bottom_bar.show_audio_device_choices(false);
return; return;
} }
@ -273,34 +280,31 @@ public class Dino.Ui.CallWindowController : Object {
update_current_audio_device(audio_settings_popover); update_current_audio_device(audio_settings_popover);
audio_settings_popover.microphone_selected.connect((device) => { 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); update_current_audio_device(audio_settings_popover);
}); });
audio_settings_popover.speaker_selected.connect((device) => { 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); update_current_audio_device(audio_settings_popover);
}); });
calls.stream_created.connect((call, media) => { // calls.stream_created.connect((call, media) => {
if (media == "audio") { // if (media == "audio") {
update_current_audio_device(audio_settings_popover); // update_current_audio_device(audio_settings_popover);
} // }
});*/ // });
} }
/*private void update_current_audio_device(AudioSettingsPopover audio_settings_popover) { private void update_current_audio_device(AudioSettingsPopover audio_settings_popover) {
Xmpp.Xep.JingleRtp.Stream stream = calls.get_audio_stream(call); audio_settings_popover.current_microphone_device = call_state.get_microphone_device();
if (stream != null) { audio_settings_popover.current_speaker_device = call_state.get_speaker_device();
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_video_device_choices() { private void update_video_device_choices() {
int device_count = call_plugin.get_devices("video", false).size; int device_count = call_plugin.get_devices("video", false).size;
if (device_count == 0) { if (device_count == 0) {
call_window.bottom_bar.show_video_device_error(); 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); call_window.bottom_bar.show_video_device_choices(false);
return; return;
} }
@ -309,23 +313,20 @@ public class Dino.Ui.CallWindowController : Object {
update_current_video_device(video_settings_popover); update_current_video_device(video_settings_popover);
video_settings_popover.camera_selected.connect((device) => { 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); update_current_video_device(video_settings_popover);
own_video.display_device(device); own_video.display_device(device);
}); });
calls.stream_created.connect((call, media) => { // call_state.stream_created.connect((call, media) => {
if (media == "video") { // if (media == "video") {
update_current_video_device(video_settings_popover); // update_current_video_device(video_settings_popover);
} // }
});*/ // });
} }
/*private void update_current_video_device(VideoSettingsPopover video_settings_popover) { private void update_current_video_device(VideoSettingsPopover video_settings_popover) {
Xmpp.Xep.JingleRtp.Stream stream = calls.get_video_stream(call); video_settings_popover.current_device = call_state.get_video_device();
if (stream != null) {
video_settings_popover.current_device = call_plugin.get_device(stream, false);
} }
}*/
private void update_own_video() { private void update_own_video() {
if (this.call_window.bottom_bar.video_enabled) { 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"); incoming_call_revealer.get_style_context().remove_class("incoming");
outer_additional_box.get_style_context().remove_class("incoming-call-box"); 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: case Call.State.RINGING:
image.set_from_icon_name("dino-phone-ring-symbolic", IconSize.LARGE_TOOLBAR); image.set_from_icon_name("dino-phone-ring-symbolic", IconSize.LARGE_TOOLBAR);
if (call.direction == Call.DIRECTION_INCOMING) { if (call.direction == Call.DIRECTION_INCOMING) {
bool video = call_manager.should_we_send_video(); bool video = call_manager.should_we_send_video();
title_label.label = video ? _("Incoming video call") : _("Incoming call"); title_label.label = video ? _("Incoming video call") : _("Incoming call");
if (stream_interactor.get_module(Calls.IDENTITY).can_we_do_calls(call.account)) {
subtitle_label.label = "Ring ring…!"; subtitle_label.label = "Ring ring…!";
incoming_call_box.visible = true; incoming_call_box.visible = true;
incoming_call_revealer.reveal_child = true; incoming_call_revealer.reveal_child = true;
incoming_call_revealer.get_style_context().add_class("incoming"); incoming_call_revealer.get_style_context().add_class("incoming");
outer_additional_box.get_style_context().add_class("incoming-call-box"); outer_additional_box.get_style_context().add_class("incoming-call-box");
} else {
subtitle_label.label = "Dependencies for call support not met";
}
} else { } else {
title_label.label = _("Calling…"); title_label.label = _("Calling…");
subtitle_label.label = "Ring ring…?"; subtitle_label.label = "Ring ring…?";

View file

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

View file

@ -101,11 +101,18 @@ namespace Dino.Plugins.Omemo.DtlsSrtpVerificationDraft {
if (fingerprint_node == null) continue; if (fingerprint_node == null) continue;
string fingerprint = fingerprint_node.get_deep_string_content(); string fingerprint = fingerprint_node.get_deep_string_content();
StanzaNode? encrypted_node = null;
try {
Xep.Omemo.OmemoEncryptor encryptor = stream.get_module(Xep.Omemo.OmemoEncryptor.IDENTITY); Xep.Omemo.OmemoEncryptor encryptor = stream.get_module(Xep.Omemo.OmemoEncryptor.IDENTITY);
Xep.Omemo.EncryptionData enc_data = encryptor.encrypt_plaintext(fingerprint); Xep.Omemo.EncryptionData enc_data = encryptor.encrypt_plaintext(fingerprint);
encryptor.encrypt_key(enc_data, iq.to.bare_jid, device_id_by_jingle_sid[sid]); 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? 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); 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); 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(); Gst.deinit();
} }
public bool supports(string media) { public bool supports(string? media) {
if (!codec_util.is_element_supported("rtpbin")) return false; if (!codec_util.is_element_supported("rtpbin")) return false;
if (media == "audio") { 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) { public Gee.List<MediaDevice> get_devices(string media, bool incoming) {
if (media == "video" && !incoming) { if (media == "video" && !incoming) {
return get_video_sources(); 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) { 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) StanzaNode item_node = new StanzaNode.build("item", NS_URI_ADMIN)
.put_attribute("affiliation", new_affiliation, 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); 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); 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) { public override void attach(XmppStream stream) {
stream.add_flag(new Flag()); 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_available.connect(on_received_available);
stream.get_module(Presence.Module.IDENTITY).received_unavailable.connect(on_received_unavailable); 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() { public override string get_ns() {
return NS_URI; 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_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 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()); 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")); invite_node.put_node(new StanzaNode.build("description", Xep.JingleRtp.NS_URI).add_self_xmlns().put_attribute("media", "audio"));
if (video) { if (video) {
@ -23,27 +23,27 @@ namespace Xmpp.Xep.MujiMeta {
stream.get_module(MessageModule.IDENTITY).send_message.begin(stream, invite_message); 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); 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); send_jmi_message(stream, "accept", invitor, muc_jid, message_type);
} }
public void send_invite_accept_to_self(XmppStream stream, Jid muc_jid) { 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); send_jmi_message(stream, "reject", invitor, muc_jid, message_type);
} }
public void send_invite_reject_to_self(XmppStream stream, Jid muc_jid) { 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 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); var muji_node = new StanzaNode.build("muji", NS_URI).add_self_xmlns().put_node(jmi_node);