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/membersdrawer.dart';
7 : import 'package:cwtch/widgets/messageloadingbubble.dart';
8 : import 'package:flutter/material.dart';
9 : import 'package:provider/provider.dart';
10 : import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
11 : import 'package:cwtch/l10n/app_localizations.dart';
12 : import '../main.dart';
13 : import '../settings.dart';
14 :
15 : class MessageList extends StatefulWidget {
16 : ItemPositionsListener scrollListener;
17 0 : MessageList(this.scrollListener);
18 :
19 0 : @override
20 0 : _MessageListState createState() => _MessageListState();
21 : }
22 :
23 : class _MessageListState extends State<MessageList> {
24 0 : @override
25 : Widget build(BuildContext outerContext) {
26 : // 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
27 0 : if (Provider.of<ContactInfoState>(context).messageCache.indexUnsynced != 0) {
28 0 : var conversationId = Provider.of<AppState>(outerContext, listen: false).selectedConversation!;
29 0 : MessageCache? cache = Provider.of<ProfileInfoState>(outerContext, listen: false).contactList.getContact(conversationId)?.messageCache;
30 0 : ByIndex(0).loadUnsynced(Provider.of<FlwtchState>(context, listen: false).cwtch, Provider.of<AppState>(outerContext, listen: false).selectedProfile!, conversationId, cache!);
31 : }
32 0 : if (Provider.of<ContactInfoState>(outerContext, listen: false).everFetched == false) {
33 0 : messageHandler(
34 : outerContext,
35 0 : Provider.of<ProfileInfoState>(outerContext, listen: false).onion,
36 0 : Provider.of<ContactInfoState>(outerContext, listen: false).identifier,
37 0 : ByIndex(Provider.of<ContactInfoState>(outerContext, listen: false).uiLoadedMessages),
38 : );
39 0 : Provider.of<ContactInfoState>(outerContext, listen: false).everFetched = true;
40 : }
41 0 : bool isP2P = !Provider.of<ContactInfoState>(context).isGroup;
42 0 : bool isGroupAndSyncing = Provider.of<ContactInfoState>(context).isGroup == true && Provider.of<ContactInfoState>(context).status == "Authenticated";
43 :
44 0 : bool preserveHistoryByDefault = Provider.of<Settings>(context, listen: false).preserveHistoryByDefault;
45 0 : bool showEphemeralWarning = (isP2P && (!preserveHistoryByDefault && Provider.of<ContactInfoState>(context).savePeerHistory != "SaveHistory"));
46 0 : bool showOfflineWarning = Provider.of<ContactInfoState>(context).isOnline() == false;
47 : bool showSyncing = isGroupAndSyncing;
48 0 : bool showHybridDisabled = Provider.of<ContactInfoState>(context).isManaged && !Provider.of<Settings>(context).isExperimentEnabled(GroupManagerExperiment);
49 : bool showMessageWarning = showEphemeralWarning || showOfflineWarning || showSyncing || showHybridDisabled;
50 :
51 0 : return RepaintBoundary(
52 0 : child: Container(
53 0 : color: Provider.of<Settings>(context).theme.backgroundMainColor,
54 0 : child: Column(
55 0 : children: [
56 0 : Visibility(
57 : visible: showMessageWarning,
58 0 : child: Container(
59 0 : padding: EdgeInsets.all(5.0),
60 0 : color: Provider.of<Settings>(context).theme.backgroundHilightElementColor,
61 0 : child: DefaultTextStyle(
62 0 : style: TextStyle(color: Provider.of<Settings>(context).theme.hilightElementColor),
63 0 : child: _chooseWarningMessage(showSyncing, showOfflineWarning, showEphemeralWarning, showHybridDisabled),
64 : ),
65 : ),
66 : ),
67 0 : Expanded(
68 0 : child: Container(
69 : // Only show broken heart is the contact is offline...
70 0 : decoration: BoxDecoration(
71 0 : image: Provider.of<ContactInfoState>(outerContext).isOnline()
72 0 : ? (Provider.of<Settings>(context).themeImages && Provider.of<Settings>(context).theme.chatImage != null)
73 0 : ? DecorationImage(
74 : repeat: ImageRepeat.repeat,
75 0 : image: Provider.of<Settings>(context).theme.loadImage(Provider.of<Settings>(context).theme.chatImage, context: context),
76 0 : colorFilter: ColorFilter.mode(Provider.of<Settings>(context).theme.chatImageColor, BlendMode.srcIn),
77 : )
78 : : null
79 0 : : DecorationImage(
80 : fit: BoxFit.scaleDown,
81 : alignment: Alignment.center,
82 0 : image: AssetImage("assets/core/negative_heart_512px.png"),
83 0 : colorFilter: ColorFilter.mode(Provider.of<Settings>(context).theme.hilightElementColor.withOpacity(0.15), BlendMode.srcIn),
84 : ),
85 : ),
86 : // Don't load messages for syncing server...
87 0 : child: Padding(padding: EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 20.0), child: _buildPaneStructure(outerContext)),
88 : ),
89 : ),
90 : ],
91 : ),
92 : ),
93 : );
94 : }
95 :
96 : // Only show one warning. This method encapsulates the logic of which warning to prioritize.
97 0 : Widget _chooseWarningMessage(showSyncing, showOfflineWarning, showEphemeralWarning, showHybridDisabled) {
98 : if (showHybridDisabled) {
99 0 : return Text(AppLocalizations.of(context)!.hybridNotEnabledWarning, textAlign: TextAlign.center);
100 : }
101 :
102 : if (showSyncing) {
103 0 : return Text(AppLocalizations.of(context)!.serverNotSynced, textAlign: TextAlign.center);
104 : }
105 :
106 : if (showOfflineWarning) {
107 : // if it's been more than 2 minutes since we last clicked the button let users click the button again
108 : // OR if users have never clicked the button AND they appear offline, then they can click the button
109 : // 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)
110 : // any significant changes in state e.g. peer offline or button clicks will trigger a rebuild anyway
111 : bool canReconnect =
112 0 : DateTime.now().difference(Provider.of<ContactInfoState>(context, listen: false).lastRetryTime).abs() > Duration(seconds: 30) ||
113 0 : (Provider.of<ProfileInfoState>(context, listen: false).appearOffline &&
114 0 : (Provider.of<ContactInfoState>(context, listen: false).lastRetryTime == Provider.of<ContactInfoState>(context, listen: false).loaded));
115 0 : var reconnectButton = Padding(
116 0 : padding: EdgeInsets.all(2),
117 : child: canReconnect
118 0 : ? Tooltip(
119 0 : message: AppLocalizations.of(context)!.retryConnectionTooltip,
120 0 : child: ElevatedButton(
121 0 : style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
122 0 : child: Text(AppLocalizations.of(context)!.retryConnection),
123 0 : onPressed: () {
124 0 : if (Provider.of<ContactInfoState>(context, listen: false).isGroup) {
125 0 : Provider.of<FlwtchState>(
126 0 : context,
127 : listen: false,
128 0 : ).cwtch.AttemptReconnectionServer(Provider.of<ProfileInfoState>(context, listen: false).onion, Provider.of<ContactInfoState>(context, listen: false).server!);
129 : } else {
130 0 : Provider.of<FlwtchState>(
131 0 : context,
132 : listen: false,
133 0 : ).cwtch.AttemptReconnection(Provider.of<ProfileInfoState>(context, listen: false).onion, Provider.of<ContactInfoState>(context, listen: false).onion);
134 : }
135 0 : Provider.of<ContactInfoState>(context, listen: false).lastRetryTime = DateTime.now();
136 0 : Provider.of<ContactInfoState>(context, listen: false).contactEvents.add(ContactEvent("Actively Retried Connection"));
137 0 : setState(() {
138 : // force update of this view...otherwise the button won't be removed fast enough...
139 : });
140 : },
141 : ),
142 : )
143 0 : : CircularProgressIndicator(color: Provider.of<Settings>(context).theme.hilightElementColor),
144 : );
145 0 : return Column(
146 : crossAxisAlignment: CrossAxisAlignment.center,
147 0 : children: [
148 0 : Text(
149 0 : Provider.of<ContactInfoState>(context).isGroup ? AppLocalizations.of(context)!.serverConnectivityDisconnected : AppLocalizations.of(context)!.peerOfflineMessage,
150 : textAlign: TextAlign.center,
151 : ),
152 : reconnectButton,
153 : ],
154 : );
155 : }
156 :
157 : if (showEphemeralWarning) {
158 : // Only show the ephemeral status for peer conversations, not for groups...
159 0 : return Text(AppLocalizations.of(context)!.chatHistoryDefault, textAlign: TextAlign.center);
160 : }
161 :
162 : // We are not allowed to put null here, so put an empty text widget
163 0 : return Text('');
164 : }
165 :
166 0 : Widget _buildPaneStructure(BuildContext outerContext) {
167 0 : if (Provider.of<ContactInfoState>(context).membersDrawerOpen) {
168 0 : return Flex(
169 : direction: Axis.horizontal,
170 0 : children: <Widget>[
171 0 : Flexible(flex: 4, child: _theList(outerContext)),
172 0 : Flexible(flex: 1, child: MembersDrawer(outerContext)),
173 : ],
174 : );
175 : } else {
176 0 : return _theList(outerContext);
177 : }
178 : }
179 :
180 0 : Widget _theList(BuildContext outerContext) {
181 0 : var initi = Provider.of<AppState>(outerContext, listen: false).initialScrollIndex;
182 0 : return ScrollablePositionedList.builder(
183 0 : itemPositionsListener: widget.scrollListener,
184 0 : itemScrollController: Provider.of<ContactInfoState>(outerContext).messageScrollController,
185 0 : initialScrollIndex: initi > 4 ? initi - 4 : 0,
186 0 : itemCount: Provider.of<ContactInfoState>(outerContext).uiLoadedMessages + 1,
187 : reverse: true, // NOTE: There seems to be a bug in flutter that corrects the mouse wheel scroll, but not the drag direction...
188 : shrinkWrap: true,
189 0 : itemBuilder: (itemBuilderContext, index) {
190 0 : var profileOnion = Provider.of<ProfileInfoState>(itemBuilderContext, listen: false).onion;
191 0 : var contactHandle = Provider.of<ContactInfoState>(itemBuilderContext, listen: false).identifier;
192 : var messageIndex = index;
193 0 : var loadedMessages = Provider.of<ContactInfoState>(itemBuilderContext, listen: false).uiLoadedMessages;
194 0 : var endOfMessages = Provider.of<ContactInfoState>(itemBuilderContext, listen: false).totalMessages + 1;
195 0 : if (index == endOfMessages - 1) {
196 0 : return ListTile(title: Text(AppLocalizations.of(itemBuilderContext)!.messageHistoryEndOfHistory, textAlign: TextAlign.center), dense: true);
197 0 : } else if (index >= loadedMessages) {
198 0 : return ElevatedButton(
199 0 : child: Text(AppLocalizations.of(itemBuilderContext)!.messageHistoryLoadOlderMessages),
200 0 : key: Key("loadAdditionalMessages"),
201 0 : onPressed: () {
202 0 : messageHandler(itemBuilderContext, profileOnion, contactHandle, ByIndex(messageIndex));
203 : },
204 : );
205 : }
206 0 : return FutureBuilder(
207 0 : future: messageHandler(itemBuilderContext, profileOnion, contactHandle, ByIndex(messageIndex)),
208 0 : builder: (fbcontext, snapshot) {
209 0 : if (snapshot.hasData) {
210 0 : var message = snapshot.data as Message;
211 : // here we create an index key for the contact and assign it to the row. Indexes are unique so we can
212 : // reliably use this without running into duplicate keys...it isn't ideal as it means keys need to be re-built
213 : // when new messages are added...however it is better than the alternative of not having widget keys at all.
214 0 : var key = Provider.of<ContactInfoState>(itemBuilderContext, listen: false).getMessageKey(contactHandle, messageIndex);
215 0 : return message.getWidget(fbcontext, key, messageIndex);
216 : } else {
217 0 : return MessageLoadingBubble();
218 : }
219 : },
220 : );
221 : },
222 : );
223 : }
224 : }
|