Line data Source code
1 : import 'package:cwtch/models/appstate.dart';
2 : import 'package:cwtch/models/contact.dart';
3 : import 'package:cwtch/models/message.dart';
4 : import 'package:cwtch/models/messagecache.dart';
5 : import 'package:cwtch/models/profile.dart';
6 : import 'package:cwtch/widgets/messageloadingbubble.dart';
7 : import 'package:flutter/material.dart';
8 : import 'package:provider/provider.dart';
9 : import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
10 : import 'package:flutter_gen/gen_l10n/app_localizations.dart';
11 : import '../main.dart';
12 : import '../settings.dart';
13 :
14 : class MessageList extends StatefulWidget {
15 : ItemPositionsListener scrollListener;
16 0 : MessageList(this.scrollListener);
17 :
18 0 : @override
19 0 : _MessageListState createState() => _MessageListState();
20 : }
21 :
22 : class _MessageListState extends State<MessageList> {
23 0 : @override
24 : Widget build(BuildContext outerContext) {
25 : // On Android we can have unsynced messages at the front of the index from when the UI was asleep, if there are some, kick off sync of those first
26 0 : if (Provider.of<ContactInfoState>(context).messageCache.indexUnsynced != 0) {
27 0 : var conversationId = Provider.of<AppState>(outerContext, listen: false).selectedConversation!;
28 0 : MessageCache? cache = Provider.of<ProfileInfoState>(outerContext, listen: false).contactList.getContact(conversationId)?.messageCache;
29 0 : ByIndex(0).loadUnsynced(Provider.of<FlwtchState>(context, listen: false).cwtch, Provider.of<AppState>(outerContext, listen: false).selectedProfile!, conversationId, cache!);
30 : }
31 0 : var initi = Provider.of<AppState>(outerContext, listen: false).initialScrollIndex;
32 0 : bool isP2P = !Provider.of<ContactInfoState>(context).isGroup;
33 0 : bool isGroupAndSyncing = Provider.of<ContactInfoState>(context).isGroup == true && Provider.of<ContactInfoState>(context).status == "Authenticated";
34 :
35 0 : bool preserveHistoryByDefault = Provider.of<Settings>(context, listen: false).preserveHistoryByDefault;
36 0 : bool showEphemeralWarning = (isP2P && (!preserveHistoryByDefault && Provider.of<ContactInfoState>(context).savePeerHistory != "SaveHistory"));
37 0 : bool showOfflineWarning = Provider.of<ContactInfoState>(context).isOnline() == false;
38 : bool showSyncing = isGroupAndSyncing;
39 : bool showMessageWarning = showEphemeralWarning || showOfflineWarning || showSyncing;
40 : // We used to only load historical messages when the conversation is with a p2p contact OR the conversation is a server and *not* syncing.
41 : // With the message cache in place this is no longer necessary
42 : bool loadMessages = true;
43 :
44 : // if it's been more than 2 minutes since we last clicked the button let users click the button again
45 : // OR if users have never clicked the button AND they appear offline, then they can click the button
46 : // NOTE: all these listeners are false...this is not ideal, but if they were true we would end up rebuilding the message view every tick (which would kill performance)
47 : // any significant changes in state e.g. peer offline or button clicks will trigger a rebuild anyway
48 0 : bool canReconnect = DateTime.now().difference(Provider.of<ContactInfoState>(context, listen: false).lastRetryTime).abs() > Duration(seconds: 30) ||
49 0 : (Provider.of<ProfileInfoState>(context, listen: false).appearOffline &&
50 0 : (Provider.of<ContactInfoState>(context, listen: false).lastRetryTime == Provider.of<ContactInfoState>(context, listen: false).loaded));
51 :
52 0 : var reconnectButton = Padding(
53 0 : padding: EdgeInsets.all(2),
54 : child: canReconnect
55 0 : ? Tooltip(
56 0 : message: AppLocalizations.of(context)!.retryConnectionTooltip,
57 0 : child: ElevatedButton(
58 0 : style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
59 0 : child: Text(AppLocalizations.of(context)!.retryConnection),
60 0 : onPressed: () {
61 0 : if (Provider.of<ContactInfoState>(context, listen: false).isGroup) {
62 0 : Provider.of<FlwtchState>(context, listen: false)
63 0 : .cwtch
64 0 : .AttemptReconnectionServer(Provider.of<ProfileInfoState>(context, listen: false).onion, Provider.of<ContactInfoState>(context, listen: false).server!);
65 : } else {
66 0 : Provider.of<FlwtchState>(context, listen: false)
67 0 : .cwtch
68 0 : .AttemptReconnection(Provider.of<ProfileInfoState>(context, listen: false).onion, Provider.of<ContactInfoState>(context, listen: false).onion);
69 : }
70 0 : Provider.of<ContactInfoState>(context, listen: false).lastRetryTime = DateTime.now();
71 0 : Provider.of<ContactInfoState>(context, listen: false).contactEvents.add(ContactEvent("Actively Retried Connection"));
72 0 : setState(() {
73 : // force update of this view...otherwise the button won't be removed fast enough...
74 : });
75 : },
76 : ))
77 0 : : CircularProgressIndicator(color: Provider.of<Settings>(context).theme.defaultButtonTextColor));
78 :
79 0 : return RepaintBoundary(
80 0 : child: Container(
81 0 : color: Provider.of<Settings>(context).theme.backgroundMainColor,
82 0 : child: Column(children: [
83 0 : Visibility(
84 : visible: showMessageWarning,
85 0 : child: Container(
86 0 : padding: EdgeInsets.all(5.0),
87 0 : color: Provider.of<Settings>(context).theme.defaultButtonActiveColor,
88 0 : child: DefaultTextStyle(
89 0 : style: TextStyle(color: Provider.of<Settings>(context).theme.defaultButtonTextColor),
90 : child: showSyncing
91 0 : ? Text(AppLocalizations.of(context)!.serverNotSynced, textAlign: TextAlign.center)
92 : : showOfflineWarning
93 0 : ? Column(crossAxisAlignment: CrossAxisAlignment.center, children: [
94 0 : Text(
95 0 : Provider.of<ContactInfoState>(context).isGroup
96 0 : ? AppLocalizations.of(context)!.serverConnectivityDisconnected
97 0 : : AppLocalizations.of(context)!.peerOfflineMessage,
98 : textAlign: TextAlign.center),
99 : reconnectButton
100 : ])
101 : // Only show the ephemeral status for peer conversations, not for groups...
102 : : (showEphemeralWarning
103 0 : ? Text(AppLocalizations.of(context)!.chatHistoryDefault, textAlign: TextAlign.center)
104 : :
105 : // We are not allowed to put null here, so put an empty text widget
106 0 : Text("")),
107 : ))),
108 0 : Expanded(
109 0 : child: Container(
110 : // Only show broken heart is the contact is offline...
111 0 : decoration: BoxDecoration(
112 0 : image: Provider.of<ContactInfoState>(outerContext).isOnline()
113 0 : ? (Provider.of<Settings>(context).themeImages && Provider.of<Settings>(context).theme.chatImage != null)
114 0 : ? DecorationImage(
115 : repeat: ImageRepeat.repeat,
116 0 : image: Provider.of<Settings>(context, listen: false).theme.loadImage(Provider.of<Settings>(context, listen: false).theme.chatImage, context: context),
117 0 : colorFilter: ColorFilter.mode(Provider.of<Settings>(context).theme.chatImageColor, BlendMode.srcIn))
118 : : null
119 0 : : DecorationImage(
120 : fit: BoxFit.scaleDown,
121 : alignment: Alignment.center,
122 0 : image: AssetImage("assets/core/negative_heart_512px.png"),
123 0 : colorFilter: ColorFilter.mode(Provider.of<Settings>(context).theme.hilightElementColor.withOpacity(0.15), BlendMode.srcIn))),
124 : // Don't load messages for syncing server...
125 0 : child: Padding(
126 0 : padding: EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 20.0),
127 : child: loadMessages
128 0 : ? ScrollablePositionedList.builder(
129 0 : itemPositionsListener: widget.scrollListener,
130 0 : itemScrollController: Provider.of<ContactInfoState>(outerContext).messageScrollController,
131 0 : initialScrollIndex: initi > 4 ? initi - 4 : 0,
132 0 : itemCount: Provider.of<ContactInfoState>(outerContext).totalMessages,
133 : reverse: true, // NOTE: There seems to be a bug in flutter that corrects the mouse wheel scroll, but not the drag direction...
134 0 : itemBuilder: (itemBuilderContext, index) {
135 0 : var profileOnion = Provider.of<ProfileInfoState>(itemBuilderContext, listen: false).onion;
136 0 : var contactHandle = Provider.of<ContactInfoState>(itemBuilderContext, listen: false).identifier;
137 : var messageIndex = index;
138 :
139 0 : return FutureBuilder(
140 0 : future: messageHandler(itemBuilderContext, profileOnion, contactHandle, ByIndex(messageIndex)),
141 0 : builder: (fbcontext, snapshot) {
142 0 : if (snapshot.hasData) {
143 0 : var message = snapshot.data as Message;
144 : // here we create an index key for the contact and assign it to the row. Indexes are unique so we can
145 : // reliably use this without running into duplicate keys...it isn't ideal as it means keys need to be re-built
146 : // when new messages are added...however it is better than the alternative of not having widget keys at all.
147 0 : var key = Provider.of<ContactInfoState>(itemBuilderContext, listen: false).getMessageKey(contactHandle, messageIndex);
148 0 : return message.getWidget(fbcontext, key, messageIndex);
149 : } else {
150 0 : return MessageLoadingBubble();
151 : }
152 : },
153 : );
154 : },
155 : )
156 : : null)))
157 : ])));
158 : }
159 : }
|