Line data Source code
1 : import 'dart:async';
2 :
3 : import 'package:cwtch/main.dart';
4 : import 'package:cwtch/models/groupmembers.dart';
5 : import 'package:cwtch/models/message_draft.dart';
6 : import 'package:cwtch/models/profile.dart';
7 : import 'package:cwtch/models/redaction.dart';
8 : import 'package:cwtch/themes/opaque.dart';
9 : import 'package:cwtch/views/contactsview.dart';
10 : import 'package:cwtch/widgets/messagerow.dart';
11 : import 'package:flutter/material.dart';
12 : import 'package:flutter/widgets.dart';
13 : import 'package:cwtch/l10n/app_localizations.dart';
14 : import 'package:provider/provider.dart';
15 : import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
16 :
17 : import 'messagecache.dart';
18 :
19 : enum ConversationNotificationPolicy { Default, OptIn, Never }
20 :
21 : extension Nameable on ConversationNotificationPolicy {
22 0 : String toName(BuildContext context) {
23 : switch (this) {
24 0 : case ConversationNotificationPolicy.Default:
25 0 : return AppLocalizations.of(context)!.conversationNotificationPolicyDefault;
26 0 : case ConversationNotificationPolicy.OptIn:
27 0 : return AppLocalizations.of(context)!.conversationNotificationPolicyOptIn;
28 0 : case ConversationNotificationPolicy.Never:
29 0 : return AppLocalizations.of(context)!.conversationNotificationPolicyNever;
30 : }
31 : }
32 : }
33 :
34 : class ContactInfoState extends ChangeNotifier {
35 : final String profileOnion;
36 : final int identifier;
37 : final String onion;
38 : late String _nickname;
39 : late String _localNickname;
40 :
41 : late ConversationNotificationPolicy _notificationPolicy;
42 :
43 : late bool _accepted;
44 : late bool _blocked;
45 : late bool _managed;
46 : late String _status;
47 : late String _imagePath;
48 : late String _defaultImagePath;
49 : late String _savePeerHistory;
50 : late int _unreadMessages = 0;
51 : late int _totalMessages = 0;
52 : bool _everFetched = false;
53 : late DateTime _lastMessageReceivedTime; // last time we received a message, for sorting
54 : late DateTime _lastMessageSentTime; // last time a message reported being sent, for display
55 : late Map<String, GlobalKey<MessageRowState>> keys;
56 : int _newMarkerMsgIndex = -1;
57 : late MessageCache messageCache;
58 : ItemScrollController messageScrollController = new ItemScrollController();
59 :
60 : // todo: a nicer way to model contacts, groups and other "entities"
61 : late bool _isGroup;
62 : bool _isShadowed = false;
63 : String? _server;
64 : late bool _archived;
65 : late bool _pinned;
66 :
67 : late String _modeLine;
68 : late String _modeMask;
69 : late List<GroupMember> _members;
70 : bool _membersDrawerOpen = false;
71 : bool _groupOpPending = false;
72 : Timer? slowModeTimer = null;
73 :
74 : int _antispamTickets = 0;
75 : String? _acnCircuit;
76 : MessageDraft _messageDraft = MessageDraft.empty();
77 :
78 : var _hoveredIndex = -1;
79 : var _pendingScroll = -1;
80 :
81 : DateTime _lastRetryTime = DateTime.now();
82 : DateTime loaded = DateTime.now();
83 :
84 : List<ContactEvent> contactEvents = List.empty(growable: true);
85 :
86 0 : ContactInfoState(
87 : this.profileOnion,
88 : this.identifier,
89 : this.onion, {
90 : nickname = "",
91 : localNickname = "",
92 : isGroup = false,
93 : accepted = false,
94 : blocked = false,
95 : status = "",
96 : imagePath = "",
97 : defaultImagePath = "",
98 : savePeerHistory = "DeleteHistoryConfirmed",
99 : numMessages = 0,
100 : numUnread = 0,
101 : lastMessageTime,
102 : server,
103 : archived = false,
104 : notificationPolicy = "ConversationNotificationPolicy.Default",
105 : pinned = false,
106 : isManaged = false,
107 : }) {
108 0 : this._nickname = nickname;
109 0 : this._localNickname = localNickname;
110 0 : this._isGroup = isGroup;
111 0 : this._accepted = accepted;
112 0 : this._blocked = blocked;
113 0 : this._status = status;
114 0 : this._imagePath = imagePath;
115 0 : this._defaultImagePath = defaultImagePath;
116 0 : this._totalMessages = numMessages;
117 0 : this._unreadMessages = numUnread;
118 0 : this._savePeerHistory = savePeerHistory;
119 0 : this._lastMessageReceivedTime = lastMessageTime == null ? DateTime.fromMillisecondsSinceEpoch(0) : lastMessageTime;
120 0 : this._lastMessageSentTime = _lastMessageReceivedTime;
121 0 : this._server = server;
122 0 : this._archived = archived;
123 0 : this._notificationPolicy = notificationPolicyFromString(notificationPolicy);
124 0 : this.messageCache = new MessageCache(_totalMessages);
125 0 : this._pinned = pinned;
126 0 : this._managed = isManaged;
127 0 : this._modeLine = "unset"; //TODO
128 0 : this._modeMask = "unset";
129 0 : keys = Map<String, GlobalKey<MessageRowState>>();
130 0 : this._members = List<GroupMember>.empty(growable: true);
131 : }
132 :
133 0 : String get nickname {
134 0 : if (this._localNickname != "") {
135 0 : return this._localNickname;
136 : }
137 0 : return this._nickname;
138 : }
139 :
140 : // hybrid groups need to access the remote groupbot's "authoritative" name setting so mods can edit it
141 0 : String get remoteNickname {
142 0 : return this._nickname;
143 : }
144 :
145 0 : String get savePeerHistory => this._savePeerHistory;
146 :
147 0 : String? get acnCircuit => this._acnCircuit;
148 :
149 0 : MessageDraft get messageDraft => this._messageDraft;
150 :
151 0 : DateTime get lastRetryTime => this._lastRetryTime;
152 0 : set lastRetryTime(DateTime lastRetryTime) {
153 0 : this._lastRetryTime = lastRetryTime;
154 0 : notifyListeners();
155 : }
156 :
157 0 : set antispamTickets(int antispamTickets) {
158 0 : this._antispamTickets = antispamTickets;
159 0 : notifyListeners();
160 : }
161 :
162 0 : int get antispamTickets => this._antispamTickets;
163 :
164 0 : set acnCircuit(String? acnCircuit) {
165 0 : this._acnCircuit = acnCircuit;
166 0 : notifyListeners();
167 : }
168 :
169 : // Indicated whether the conversation is archived, in which case it will
170 : // be moved to the very bottom of the active conversations list until
171 : // new messages appear
172 0 : set isArchived(bool archived) {
173 0 : this._archived = archived;
174 0 : notifyListeners();
175 : }
176 :
177 0 : bool get isArchived => this._archived;
178 :
179 0 : set savePeerHistory(String newVal) {
180 0 : this._savePeerHistory = newVal;
181 0 : notifyListeners();
182 : }
183 :
184 0 : set nickname(String newVal) {
185 0 : this._nickname = newVal;
186 0 : notifyListeners();
187 : }
188 :
189 0 : set localNickname(String newVal) {
190 0 : this._localNickname = newVal;
191 0 : notifyListeners();
192 : }
193 :
194 0 : bool get isGroup => this._isGroup;
195 :
196 0 : set isGroup(bool newVal) {
197 0 : this._isGroup = newVal;
198 0 : notifyListeners();
199 : }
200 :
201 0 : bool get isShadowed => this._isShadowed;
202 0 : set isShadowed(bool newVal) {
203 0 : this._isShadowed = newVal;
204 0 : notifyListeners();
205 : }
206 :
207 0 : bool get membersDrawerOpen => this._membersDrawerOpen;
208 0 : set membersDrawerOpen(bool newVal) {
209 0 : this._membersDrawerOpen = newVal;
210 0 : notifyListeners();
211 : }
212 :
213 0 : bool get isOpPending => this._groupOpPending;
214 0 : set isOpPending(bool newVal) {
215 0 : this._groupOpPending = newVal;
216 0 : notifyListeners();
217 : }
218 :
219 0 : bool get isBlocked => this._blocked;
220 :
221 0 : bool get isInvitation => !this._blocked && !this._accepted;
222 :
223 0 : set accepted(bool newVal) {
224 0 : this._accepted = newVal;
225 0 : notifyListeners();
226 : }
227 :
228 0 : set blocked(bool newVal) {
229 0 : this._blocked = newVal;
230 0 : notifyListeners();
231 : }
232 :
233 0 : String get status => this._status;
234 :
235 0 : set status(String newVal) {
236 0 : this._status = newVal;
237 0 : this.contactEvents.add(ContactEvent("Update Peer Status Received: $newVal"));
238 0 : notifyListeners();
239 : }
240 :
241 0 : set messageDraft(MessageDraft newVal) {
242 0 : this._messageDraft = newVal;
243 0 : notifyListeners();
244 : }
245 :
246 0 : void notifyMessageDraftUpdate() {
247 0 : notifyListeners();
248 : }
249 :
250 0 : void selected() {
251 0 : this._newMarkerMsgIndex = this._unreadMessages - 1;
252 0 : this._unreadMessages = 0;
253 : }
254 :
255 0 : void unselected() {
256 0 : this._newMarkerMsgIndex = -1;
257 : }
258 :
259 0 : int get unreadMessages => this._unreadMessages;
260 :
261 0 : set unreadMessages(int newVal) {
262 0 : this._unreadMessages = newVal;
263 0 : notifyListeners();
264 : }
265 :
266 0 : int get newMarkerMsgIndex {
267 0 : return this._newMarkerMsgIndex;
268 : }
269 :
270 0 : int get totalMessages => this._totalMessages;
271 :
272 0 : set totalMessages(int newVal) {
273 0 : this._totalMessages = newVal;
274 0 : this.messageCache.storageMessageCount = newVal;
275 0 : notifyListeners();
276 : }
277 :
278 0 : int get uiLoadedMessages => this.messageCache.uiLoadedMessages;
279 :
280 0 : bool get everFetched => this._everFetched;
281 :
282 0 : set everFetched(bool newVal) {
283 0 : this._everFetched = newVal;
284 : }
285 :
286 0 : String get imagePath {
287 : // don't show custom images for blocked contacts..
288 0 : if (!this.isBlocked) {
289 0 : return this._imagePath;
290 : }
291 0 : return this.defaultImagePath;
292 : }
293 :
294 0 : set imagePath(String newVal) {
295 0 : this._imagePath = newVal;
296 0 : notifyListeners();
297 : }
298 :
299 0 : String get defaultImagePath => this._defaultImagePath;
300 :
301 0 : set defaultImagePath(String newVal) {
302 0 : this._defaultImagePath = newVal;
303 0 : notifyListeners();
304 : }
305 :
306 : // This is last message received time (local) and to be used for sorting only
307 : // for instance, group sync, we want to pop to the top, so we set to time.Now() for new messages
308 : // but it should not be used for display
309 0 : DateTime get lastMessageReceivedTime => this._lastMessageReceivedTime;
310 :
311 0 : set lastMessageReceivedTime(DateTime newVal) {
312 0 : this._lastMessageReceivedTime = newVal;
313 0 : notifyListeners();
314 : }
315 :
316 : // This is last message sent time and is based on message reports of sent times
317 : // this can be used to display in the contact list a last time a message was received
318 0 : DateTime get lastMessageSentTime => this._lastMessageSentTime;
319 0 : set lastMessageSentTime(DateTime newVal) {
320 0 : this._lastMessageSentTime = newVal;
321 0 : notifyListeners();
322 : }
323 :
324 : // we only allow callers to fetch the server
325 0 : String? get server => this._server;
326 :
327 0 : bool get isManaged => this._managed;
328 :
329 0 : bool isOnline() {
330 0 : if (this.isGroup == true) {
331 : // We now have an out of sync warning so we will mark these as online...
332 0 : return this.status == "Authenticated" || this.status == "Synced";
333 : } else {
334 0 : return this.status == "Authenticated";
335 : }
336 : }
337 :
338 0 : bool canSend() {
339 0 : if (this.isGroup == true) {
340 : // We now have an out of sync warning so we will mark these as online...
341 0 : return this.status == "Synced" && this.antispamTickets > 0;
342 : } else {
343 0 : return this.isOnline();
344 : }
345 : }
346 :
347 0 : ConversationNotificationPolicy get notificationsPolicy => _notificationPolicy;
348 :
349 0 : set notificationsPolicy(ConversationNotificationPolicy newVal) {
350 0 : _notificationPolicy = newVal;
351 0 : notifyListeners();
352 : }
353 :
354 0 : GlobalKey<MessageRowState> getMessageKey(int conversation, int message) {
355 0 : String index = "c: " + conversation.toString() + " m:" + message.toString();
356 0 : if (keys[index] == null) {
357 0 : keys[index] = GlobalKey<MessageRowState>();
358 : }
359 0 : GlobalKey<MessageRowState> ret = keys[index]!;
360 : return ret;
361 : }
362 :
363 0 : GlobalKey<MessageRowState>? getMessageKeyOrFail(int conversation, int message) {
364 0 : String index = "c: " + conversation.toString() + " m:" + message.toString();
365 :
366 0 : if (keys[index] == null) {
367 : return null;
368 : }
369 0 : GlobalKey<MessageRowState> ret = keys[index]!;
370 : return ret;
371 : }
372 :
373 0 : void newMessage(
374 : int identifier,
375 : int messageID,
376 : DateTime timestamp,
377 : String senderHandle,
378 : String senderImage,
379 : bool isAuto,
380 : String data,
381 : String contenthash,
382 : bool selectedConversation,
383 : String signature,
384 : ) {
385 : if (!selectedConversation) {
386 0 : unreadMessages++;
387 : }
388 0 : if (_newMarkerMsgIndex == -1) {
389 : if (!selectedConversation) {
390 0 : _newMarkerMsgIndex = 0;
391 : }
392 : } else {
393 0 : _newMarkerMsgIndex++;
394 : }
395 :
396 0 : this._lastMessageReceivedTime = timestamp;
397 0 : this._lastMessageSentTime = timestamp;
398 0 : this.messageCache.addNew(profileOnion, identifier, messageID, timestamp, senderHandle, senderImage, isAuto, data, contenthash, signature);
399 0 : this.totalMessages += 1;
400 :
401 : // We only ever see messages from authenticated peers.
402 : // If the contact is marked as offline then override this - can happen when the contact is removed from the front
403 : // end during syncing.
404 0 : if (isOnline() == false) {
405 0 : status = "Authenticated";
406 : }
407 0 : notifyListeners();
408 : }
409 :
410 0 : void ackCache(int messageID) {
411 0 : this.messageCache.ackCache(messageID);
412 0 : notifyListeners();
413 : }
414 :
415 0 : void errCache(int messageID) {
416 0 : this.messageCache.errCache(messageID);
417 0 : notifyListeners();
418 : }
419 :
420 0 : void updateMessage(int messageID, dynamic message, String? signature, dynamic attributes) {
421 0 : this.messageCache.update(messageID, message, signature, attributes);
422 0 : notifyListeners();
423 : }
424 :
425 0 : static ConversationNotificationPolicy notificationPolicyFromString(String val) {
426 : switch (val) {
427 0 : case "ConversationNotificationPolicy.Default":
428 : return ConversationNotificationPolicy.Default;
429 0 : case "ConversationNotificationPolicy.OptIn":
430 : return ConversationNotificationPolicy.OptIn;
431 0 : case "ConversationNotificationPolicy.Never":
432 : return ConversationNotificationPolicy.Never;
433 : }
434 : return ConversationNotificationPolicy.Never;
435 : }
436 :
437 0 : bool get pinned {
438 0 : return _pinned;
439 : }
440 :
441 : // Pin the conversation to the top of the conversation list
442 : // Requires caller tree to contain a FlwtchState and ProfileInfoState provider.
443 0 : void pin(context) {
444 0 : _pinned = true;
445 0 : var profileHandle = Provider.of<ProfileInfoState>(context, listen: false).onion;
446 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.SetConversationAttribute(profileHandle, identifier, "profile.pinned", "true");
447 0 : notifyListeners();
448 : }
449 :
450 : // Unpin the conversation from the top of the conversation list
451 : // Requires caller tree to contain a FlwtchState and ProfileInfoState provider.
452 0 : void unpin(context) {
453 0 : _pinned = false;
454 0 : var profileHandle = Provider.of<ProfileInfoState>(context, listen: false).onion;
455 0 : Provider.of<FlwtchState>(context, listen: false).cwtch.SetConversationAttribute(profileHandle, identifier, "profile.pinned", "false");
456 0 : notifyListeners();
457 : }
458 :
459 : // returns true only if the conversation has been accepted, and has not been blocked
460 0 : bool isAccepted() {
461 0 : return _accepted && !_blocked;
462 : }
463 :
464 : String summary = "";
465 0 : void updateSummaryEvent(String summary) {
466 0 : this.summary += summary;
467 0 : notifyListeners();
468 : }
469 :
470 0 : void updateTranslationEvent(int messageID, String translation) {
471 0 : this.messageCache.updateTranslationEvent(messageID, translation);
472 0 : notifyListeners();
473 : }
474 :
475 : // Contact Attributes. Can be set in Profile Edit View...
476 : List<String?> attributes = [null, null, null];
477 0 : void setAttribute(int i, String? value) {
478 0 : this.attributes[i] = value;
479 0 : notifyListeners();
480 : }
481 :
482 : ProfileStatusMenu availabilityStatus = ProfileStatusMenu.available;
483 0 : void setAvailabilityStatus(String status) {
484 : switch (status) {
485 0 : case "available":
486 0 : availabilityStatus = ProfileStatusMenu.available;
487 : break;
488 0 : case "busy":
489 0 : availabilityStatus = ProfileStatusMenu.busy;
490 : break;
491 0 : case "away":
492 0 : availabilityStatus = ProfileStatusMenu.away;
493 : break;
494 : default:
495 : ProfileStatusMenu.available;
496 : }
497 0 : notifyListeners();
498 : }
499 :
500 0 : Color getBorderColor(OpaqueThemeType theme) {
501 0 : if (this.isBlocked) {
502 0 : return theme.portraitBlockedBorderColor;
503 : }
504 0 : if (this.isOnline()) {
505 0 : switch (this.availabilityStatus) {
506 0 : case ProfileStatusMenu.available:
507 0 : return theme.portraitOnlineBorderColor;
508 0 : case ProfileStatusMenu.away:
509 0 : return theme.portraitOnlineAwayColor;
510 0 : case ProfileStatusMenu.busy:
511 0 : return theme.portraitOnlineBusyColor;
512 : default:
513 : // noop not a valid status...
514 : break;
515 : }
516 : }
517 0 : return theme.portraitOfflineBorderColor;
518 : }
519 :
520 0 : String augmentedNickname(BuildContext context) {
521 0 : var nick = redactedNick(context, this.onion, this.nickname);
522 0 : return nick + (this.availabilityStatus == ProfileStatusMenu.available ? "" : " (" + this.statusString(context) + ")");
523 : }
524 :
525 : // Never use this for message lookup - can be a non-indexed value
526 : // e.g. -1
527 0 : int get hoveredIndex => _hoveredIndex;
528 0 : set hoveredIndex(int newVal) {
529 0 : this._hoveredIndex = newVal;
530 0 : notifyListeners();
531 : }
532 :
533 0 : int get pendingScroll => _pendingScroll;
534 0 : set pendingScroll(int newVal) {
535 0 : this._pendingScroll = newVal;
536 0 : notifyListeners();
537 : }
538 :
539 0 : String statusString(BuildContext context) {
540 0 : switch (this.availabilityStatus) {
541 0 : case ProfileStatusMenu.available:
542 0 : return AppLocalizations.of(context)!.availabilityStatusAvailable;
543 0 : case ProfileStatusMenu.away:
544 0 : return AppLocalizations.of(context)!.availabilityStatusAway;
545 0 : case ProfileStatusMenu.busy:
546 0 : return AppLocalizations.of(context)!.availabilityStatusBusy;
547 : default:
548 0 : throw UnimplementedError("not a valid status");
549 : }
550 : }
551 :
552 0 : List<GroupMember> get members => _members;
553 0 : set members(List<GroupMember> newVal) {
554 0 : this._members = newVal;
555 0 : notifyListeners();
556 : }
557 :
558 0 : List<GroupMember> getMembersOfRole(String role) {
559 0 : return this._members.where((x) => x.role == role).toList();
560 : }
561 :
562 0 : String getRole(String onion) {
563 : try {
564 0 : return this._members.firstWhere((x) => x.onion == onion).role;
565 : } catch (e) {
566 : //member not found
567 : return '';
568 : }
569 : }
570 :
571 0 : String get modeLine => _modeLine;
572 0 : set modeLine(String newVal) {
573 0 : this._modeLine = newVal;
574 0 : notifyListeners();
575 : }
576 :
577 0 : String get modeMask => _modeMask;
578 0 : set modeMask(String newVal) {
579 0 : this._modeMask = newVal;
580 0 : notifyListeners();
581 : }
582 : }
583 :
584 : class ContactEvent {
585 : String summary;
586 : late DateTime timestamp;
587 0 : ContactEvent(this.summary) {
588 0 : this.timestamp = DateTime.now();
589 : }
590 : }
|