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