cwtch-ui/lib/widgets/messagelist.dart

165 lines
11 KiB
Dart

import 'package:cwtch/config.dart';
import 'package:cwtch/models/appstate.dart';
import 'package:cwtch/models/contact.dart';
import 'package:cwtch/models/message.dart';
import 'package:cwtch/models/messagecache.dart';
import 'package:cwtch/models/profile.dart';
import 'package:cwtch/themes/opaque.dart';
import 'package:cwtch/widgets/messageloadingbubble.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:provider/provider.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../main.dart';
import '../settings.dart';
class MessageList extends StatefulWidget {
ItemPositionsListener scrollListener;
MessageList(this.scrollListener);
@override
_MessageListState createState() => _MessageListState();
}
class _MessageListState extends State<MessageList> {
@override
Widget build(BuildContext outerContext) {
// 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
if (Provider.of<ContactInfoState>(context).messageCache.indexUnsynced != 0) {
var conversationId = Provider.of<AppState>(outerContext, listen: false).selectedConversation!;
MessageCache? cache = Provider.of<ProfileInfoState>(outerContext, listen: false).contactList.getContact(conversationId)?.messageCache;
ByIndex(0).loadUnsynced(Provider.of<FlwtchState>(context, listen: false).cwtch, Provider.of<AppState>(outerContext, listen: false).selectedProfile!, conversationId, cache!);
}
var initi = Provider.of<AppState>(outerContext, listen: false).initialScrollIndex;
bool isP2P = !Provider.of<ContactInfoState>(context).isGroup;
bool isGroupAndSyncing = Provider.of<ContactInfoState>(context).isGroup == true && Provider.of<ContactInfoState>(context).status == "Authenticated";
// Older checks, no longer used, kept for reference.
//bool isGroupAndSynced = Provider.of<ContactInfoState>(context).isGroup && Provider.of<ContactInfoState>(context).status == "Synced";
//bool isGroupAndNotAuthenticated = Provider.of<ContactInfoState>(context).isGroup && Provider.of<ContactInfoState>(context).status != "Authenticated";
bool preserveHistoryByDefault = Provider.of<Settings>(context, listen: false).preserveHistoryByDefault;
bool showEphemeralWarning = (isP2P && (!preserveHistoryByDefault && Provider.of<ContactInfoState>(context).savePeerHistory != "SaveHistory"));
bool showOfflineWarning = Provider.of<ContactInfoState>(context).isOnline() == false;
bool showSyncing = isGroupAndSyncing;
bool showMessageWarning = showEphemeralWarning || showOfflineWarning || showSyncing;
// We used to only load historical messages when the conversation is with a p2p contact OR the conversation is a server and *not* syncing.
// With the message cache in place this is no longer necessary
bool loadMessages = true;
// if it's been more than 2 minutes since we last clicked the button let users click the button again
// OR if users have never clicked the button AND they appear offline, then they can click the button
// 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)
// any significant changes in state e.g. peer offline or button clicks will trigger a rebuild anyway
bool canReconnect = DateTime.now().difference(Provider.of<ContactInfoState>(context, listen: false).lastRetryTime).abs() > Duration(seconds: 30) ||
(Provider.of<ProfileInfoState>(context, listen: false).appearOffline &&
(Provider.of<ContactInfoState>(context, listen: false).lastRetryTime == Provider.of<ContactInfoState>(context, listen: false).loaded));
var reconnectButton = Padding(
padding: EdgeInsets.all(2),
child: canReconnect
? Tooltip(
message: AppLocalizations.of(context)!.retryConnectionTooltip,
child: ElevatedButton(
style: ButtonStyle(padding: MaterialStateProperty.all(EdgeInsets.all(20))),
child: Text(AppLocalizations.of(context)!.retryConnection),
onPressed: () {
if (Provider.of<ContactInfoState>(context, listen: false).isGroup) {
Provider.of<FlwtchState>(context, listen: false)
.cwtch
.AttemptReconnectionServer(Provider.of<ProfileInfoState>(context, listen: false).onion, Provider.of<ContactInfoState>(context, listen: false).server!);
} else {
Provider.of<FlwtchState>(context, listen: false)
.cwtch
.AttemptReconnection(Provider.of<ProfileInfoState>(context, listen: false).onion, Provider.of<ContactInfoState>(context, listen: false).onion);
}
Provider.of<ContactInfoState>(context, listen: false).lastRetryTime = DateTime.now();
Provider.of<ContactInfoState>(context, listen: false).contactEvents.add(ContactEvent("Actively Retried Connection"));
setState(() {
// force update of this view...otherwise the button won't be removed fast enough...
});
},
))
: CircularProgressIndicator(color: Provider.of<Settings>(context).theme.defaultButtonTextColor));
return RepaintBoundary(
child: Container(
color: Provider.of<Settings>(context).theme.backgroundMainColor,
child: Column(children: [
Visibility(
visible: showMessageWarning,
child: Container(
padding: EdgeInsets.all(5.0),
color: Provider.of<Settings>(context).theme.defaultButtonActiveColor,
child: DefaultTextStyle(
style: TextStyle(color: Provider.of<Settings>(context).theme.defaultButtonTextColor),
child: showSyncing
? Text(AppLocalizations.of(context)!.serverNotSynced, textAlign: TextAlign.center)
: showOfflineWarning
? Column(crossAxisAlignment: CrossAxisAlignment.center, children: [
Text(
Provider.of<ContactInfoState>(context).isGroup
? AppLocalizations.of(context)!.serverConnectivityDisconnected
: AppLocalizations.of(context)!.peerOfflineMessage,
textAlign: TextAlign.center),
reconnectButton
])
// Only show the ephemeral status for peer conversations, not for groups...
: (showEphemeralWarning
? Text(AppLocalizations.of(context)!.chatHistoryDefault, textAlign: TextAlign.center)
:
// We are not allowed to put null here, so put an empty text widget
Text("")),
))),
Expanded(
child: Container(
// Only show broken heart is the contact is offline...
decoration: BoxDecoration(
image: Provider.of<ContactInfoState>(outerContext).isOnline()
? (Provider.of<Settings>(context).theme.chatImage != null)
? DecorationImage(
repeat: ImageRepeat.repeat,
image: Provider.of<Settings>(context).theme.loadImage(Provider.of<Settings>(context).theme.chatImage),
colorFilter: ColorFilter.mode(Provider.of<Settings>(context).theme.hilightElementColor.withOpacity(0.15), BlendMode.srcIn))
: null
: DecorationImage(
fit: BoxFit.scaleDown,
alignment: Alignment.center,
image: AssetImage("assets/core/negative_heart_512px.png"),
colorFilter: ColorFilter.mode(Provider.of<Settings>(context).theme.hilightElementColor.withOpacity(0.15), BlendMode.srcIn))),
// Don't load messages for syncing server...
child: loadMessages
? ScrollablePositionedList.builder(
itemPositionsListener: widget.scrollListener,
itemScrollController: Provider.of<ContactInfoState>(outerContext).messageScrollController,
initialScrollIndex: initi > 4 ? initi - 4 : 0,
itemCount: Provider.of<ContactInfoState>(outerContext).totalMessages,
reverse: true, // NOTE: There seems to be a bug in flutter that corrects the mouse wheel scroll, but not the drag direction...
itemBuilder: (itemBuilderContext, index) {
var profileOnion = Provider.of<ProfileInfoState>(itemBuilderContext, listen: false).onion;
var contactHandle = Provider.of<ContactInfoState>(itemBuilderContext, listen: false).identifier;
var messageIndex = index;
return FutureBuilder(
future: messageHandler(itemBuilderContext, profileOnion, contactHandle, ByIndex(messageIndex)),
builder: (context, snapshot) {
if (snapshot.hasData) {
var message = snapshot.data as Message;
// here we create an index key for the contact and assign it to the row. Indexes are unique so we can
// reliably use this without running into duplicate keys...it isn't ideal as it means keys need to be re-built
// when new messages are added...however it is better than the alternative of not having widget keys at all.
var key = Provider.of<ContactInfoState>(itemBuilderContext, listen: false).getMessageKey(contactHandle, messageIndex);
return message.getWidget(context, key, messageIndex);
} else {
return MessageLoadingBubble();
}
},
);
},
)
: null))
])));
}
}