aboutsummaryrefslogtreecommitdiff
path: root/main/src/ui/conversation_content_view/call_widget.vala
blob: ab047196ef74d9374c3416e13521abaf60b6eb33 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
using Gee;
using Gdk;
using Gtk;
using Pango;
using Xmpp;

using Dino.Entities;

namespace Dino.Ui {

    public class CallMetaItem : ConversationSummary.ContentMetaItem {

        private StreamInteractor stream_interactor;

        public CallMetaItem(ContentItem content_item, StreamInteractor stream_interactor) {
            base(content_item);
            this.stream_interactor = stream_interactor;
        }

        public override Object? get_widget(Plugins.ConversationItemWidgetInterface outer, Plugins.WidgetType type) {
            CallItem call_item = content_item as CallItem;
            CallState? call_state = stream_interactor.get_module(Calls.IDENTITY).call_states[call_item.call];
            return new CallWidget(stream_interactor, call_item.call, call_state, call_item.conversation);
        }

        public override Gee.List<Plugins.MessageAction>? get_item_actions(Plugins.WidgetType type) { return null; }
    }

    [GtkTemplate (ui = "/im/dino/Dino/call_widget.ui")]
    public class CallWidget : SizeRequestBox {

        [GtkChild] public unowned Image image;
        [GtkChild] public unowned Label title_label;
        [GtkChild] public unowned Label subtitle_label;
        [GtkChild] public unowned Revealer incoming_call_revealer;
        [GtkChild] public unowned Box outer_additional_box;
        [GtkChild] public unowned Box incoming_call_box;
        [GtkChild] public unowned Box multiparty_peer_box;
        [GtkChild] public unowned Button accept_call_button;
        [GtkChild] public unowned Button reject_call_button;

        private StreamInteractor stream_interactor;
        private CallState call_manager;
        private Call call;
        private Conversation conversation;
        public Call.State call_state { get; set; } // needs to be public for binding
        private uint time_update_handler_id = 0;
        private ArrayList<Widget> multiparty_peer_widgets = new ArrayList<Widget>();

        construct {
            margin_top = 4;
            size_request_mode = SizeRequestMode.HEIGHT_FOR_WIDTH;
        }

        /** @param call_state Null if it's an old call and we can't interact with it anymore */
        public CallWidget(StreamInteractor stream_interactor, Call call, CallState? call_state, Conversation conversation) {
            this.stream_interactor = stream_interactor;
            this.call_manager = call_state;
            this.call = call;
            this.conversation = conversation;

//            size_allocate.connect((allocation) => {
//                if (allocation.height > parent.get_allocated_height()) {
//                    Idle.add(() => { parent.queue_resize(); return false; });
//                }
//            });

            call.bind_property("state", this, "call-state");
            this.notify["call-state"].connect(update_call_state);

            if (call_manager != null && (call.state == Call.State.ESTABLISHING || call.state == Call.State.IN_PROGRESS)) {
                call_manager.peer_joined.connect(update_counterparts);
            }

            accept_call_button.clicked.connect(() => {
                call_manager.accept();

                var call_window = new CallWindow();
                call_window.controller = new CallWindowController(call_window, call_state, stream_interactor);
                call_window.present();
            });

            reject_call_button.clicked.connect(call_manager.reject);

            update_call_state();
        }

        private void update_counterparts() {
            if (call.state != Call.State.IN_PROGRESS && call.state != Call.State.ENDED) return;
            if (call.counterparts.size <= 1 && conversation.type_ == Conversation.Type.CHAT) return;

            foreach (Widget peer_widget in multiparty_peer_widgets) {
                multiparty_peer_box.remove(peer_widget);
            }

            foreach (Jid counterpart in call.counterparts) {
                AvatarPicture picture = new AvatarPicture() { margin_top=2 };
                picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add_participant(conversation, counterpart.bare_jid);
                multiparty_peer_box.append(picture);
                multiparty_peer_widgets.add(picture);
            }
            AvatarPicture picture2 = new AvatarPicture() { margin_top=2 };
            picture2.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add_participant(conversation, call.account.bare_jid);
            multiparty_peer_box.append(picture2);
            multiparty_peer_widgets.add(picture2);

            outer_additional_box.add_css_class("multiparty-participants");

            multiparty_peer_box.visible = true;
            incoming_call_box.visible = false;
            incoming_call_revealer.reveal_child = true;
        }

        private void update_call_state() {
            incoming_call_revealer.reveal_child = false;
            incoming_call_revealer.remove_css_class("incoming");
            outer_additional_box.remove_css_class("incoming-call-box");

            // 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");
                    if (call.direction == Call.DIRECTION_INCOMING) {
                        bool video = call_manager.should_we_send_video();

                        title_label.label = video ? _("Incoming video call") : _("Incoming call");
                        if (call_manager.invited_to_group_call != null) {
                            title_label.label = video ? _("Incoming video group call") : _("Incoming group call");
                        }

                        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.add_css_class("incoming");
                            outer_additional_box.add_css_class("incoming-call-box");
                        } else {
                            subtitle_label.label = "Dependencies for call support not met";
                        }
                    } else {
                        title_label.label = _("Calling…");
                        subtitle_label.label = "Ring ring…?";
                    }
                    break;
                case Call.State.ESTABLISHING:
                case Call.State.IN_PROGRESS:
                    image.set_from_icon_name("dino-phone-in-talk-symbolic");
                    title_label.label = _("Call started");
                    string duration = get_duration_string((new DateTime.now_utc()).difference(call.local_time));
                    subtitle_label.label = _("Started %s ago").printf(duration);

                    time_update_handler_id = Timeout.add_seconds(get_next_time_change() + 1, () => {
                        if (time_update_handler_id != 0) {
                            Source.remove(time_update_handler_id);
                            time_update_handler_id = 0;
                            update_call_state();
                        }
                        return true;
                    });

                    break;
                case Call.State.OTHER_DEVICE:
                    image.set_from_icon_name("dino-phone-hangup-symbolic");
                    title_label.label = call.direction == Call.DIRECTION_INCOMING ? _("Incoming call") : _("Outgoing call");
                    subtitle_label.label = _("You handled this call on another device");

                    break;
                case Call.State.ENDED:
                    image.set_from_icon_name("dino-phone-hangup-symbolic");
                    title_label.label = _("Call ended");
                    string formated_end = Util.format_time(call.end_time.to_local(), _("%H∶%M"), _("%l∶%M %p"));
                    string duration = get_duration_string(call.end_time.difference(call.local_time));
                    subtitle_label.label = _("Ended at %s").printf(formated_end) +
                            " · " +
                            _("Lasted %s").printf(duration);
                    break;
                case Call.State.MISSED:
                    image.set_from_icon_name("dino-phone-missed-symbolic");
                    title_label.label = _("Call missed");
                    if (call.direction == Call.DIRECTION_INCOMING) {
                        subtitle_label.label = _("You missed this call");
                    } else {
                        string who = Util.get_conversation_display_name(stream_interactor, conversation);
                        subtitle_label.label = _("%s missed this call").printf(who);
                    }
                    break;
                case Call.State.DECLINED:
                    image.set_from_icon_name("dino-phone-hangup-symbolic");
                    title_label.label = _("Call declined");
                    if (call.direction == Call.DIRECTION_INCOMING) {
                        subtitle_label.label = _("You declined this call");
                    } else {
                        string who = Util.get_conversation_display_name(stream_interactor, conversation);
                        subtitle_label.label = _("%s declined this call").printf(who);
                    }
                    break;
                case Call.State.FAILED:
                    image.set_from_icon_name("dino-phone-hangup-symbolic");
                    title_label.label = _("Call failed");
                    subtitle_label.label = "Call failed to establish";
                    break;
            }

            update_counterparts();
        }

        private string get_duration_string(TimeSpan duration) {
            DateTime a = new DateTime.now_utc();
            DateTime b = new DateTime.now_utc();
            a.difference(b);

            TimeSpan remainder_duration = duration;

            int hours = (int) Math.floor(remainder_duration / TimeSpan.HOUR);
            remainder_duration -= hours * TimeSpan.HOUR;

            int minutes = (int) Math.floor(remainder_duration / TimeSpan.MINUTE);
            remainder_duration -= minutes * TimeSpan.MINUTE;

            string ret = "";

            if (hours > 0) {
                ret += n("%i hour", "%i hours", hours).printf(hours);
            }

            if (minutes > 0) {
                if (ret.length > 0) {
                    ret += " ";
                }
                ret += n("%i minute", "%i minutes", minutes).printf(minutes);
            }

            if (ret.length > 0) {
                return ret;
            }

            return _("a few seconds");
        }

        private int get_next_time_change() {
            DateTime now = new DateTime.now_local();
            DateTime item_time = call.local_time;

            if (now.get_second() < item_time.get_second()) {
                return item_time.get_second() - now.get_second();
            } else {
                return 60 - (now.get_second() - item_time.get_second());
            }
        }

        public override void dispose() {
            base.dispose();

            if (time_update_handler_id != 0) {
                Source.remove(time_update_handler_id);
                time_update_handler_id = 0;
            }
            if (call_manager != null) {
                call_manager.peer_joined.disconnect(update_counterparts);
            }
        }
    }
}