Replace ListTile with Custom Card/Inkwell + New ProfileImage
continuous-integration/drone/pr Build is passing Details

This commit is contained in:
Sarah Jamie Lewis 2021-04-09 21:01:12 -07:00
parent 3dbfd9deb9
commit e356254ff8
11 changed files with 248 additions and 162 deletions

View File

@ -20,12 +20,7 @@ class CwtchNotifier {
void handleMessage(String type, dynamic data) { void handleMessage(String type, dynamic data) {
switch (type) { switch (type) {
case "NewPeer": case "NewPeer":
profileCN.add(ProfileInfoState( profileCN.add(ProfileInfoState(onion: data["Identity"], nickname: data["name"], imagePath: data["picture"], contactsJson: data["ContactsJson"], online: data["Online"] == "true"));
onion: data["Identity"],
nickname: data["name"],
imagePath: data["picture"],
contactsJson: data["ContactsJson"],
));
break; break;
case "PeerCreated": case "PeerCreated":
profileCN.getProfile(data["ProfileOnion"]).contactList.add(ContactInfoState( profileCN.getProfile(data["ProfileOnion"]).contactList.add(ContactInfoState(
@ -64,6 +59,17 @@ class CwtchNotifier {
case "UpdateGlobalSettings": case "UpdateGlobalSettings":
settings.handleUpdate(jsonDecode(data["Data"])); settings.handleUpdate(jsonDecode(data["Data"]));
break; break;
case "SetAttribute":
if (data["Key"] == "public.name") {
profileCN.getProfile(data["ProfileOnion"]).nickname = data["Data"];
} else {
print("unhandled set attribute event: $type");
}
break;
case "NetworkError":
var isOnline = data["Status"] == "Success";
profileCN.getProfile(data["ProfileOnion"]).isOnline = isOnline;
break;
default: default:
print("unhandled event: $type"); print("unhandled event: $type");
} }

View File

@ -124,6 +124,7 @@ class ProfileInfoState extends ChangeNotifier {
String _nickname = ""; String _nickname = "";
String _imagePath = ""; String _imagePath = "";
int _unreadMessages = 0; int _unreadMessages = 0;
bool _online = false;
ProfileInfoState({ ProfileInfoState({
this.onion, this.onion,
@ -131,10 +132,12 @@ class ProfileInfoState extends ChangeNotifier {
imagePath = "", imagePath = "",
unreadMessages = 0, unreadMessages = 0,
contactsJson = "", contactsJson = "",
online = false,
}) { }) {
this._nickname = nickname; this._nickname = nickname;
this._imagePath = imagePath; this._imagePath = imagePath;
this._unreadMessages = unreadMessages; this._unreadMessages = unreadMessages;
this._online = online;
if (contactsJson != null && contactsJson != "" && contactsJson != "null") { if (contactsJson != null && contactsJson != "" && contactsJson != "null") {
print("decoding " + contactsJson); print("decoding " + contactsJson);
@ -155,6 +158,13 @@ class ProfileInfoState extends ChangeNotifier {
} }
} }
// Getters and Setters for Online Status
bool get isOnline => this._online;
set isOnline(bool newValue) {
this._online = newValue;
notifyListeners();
}
String get nickname => this._nickname; String get nickname => this._nickname;
set nickname(String newValue) { set nickname(String newValue) {
this._nickname = newValue; this._nickname = newValue;
@ -275,7 +285,6 @@ class ContactInfoState extends ChangeNotifier {
} }
} }
class MessageState extends ChangeNotifier { class MessageState extends ChangeNotifier {
final String profileOnion; final String profileOnion;
final String contactHandle; final String contactHandle;
@ -309,7 +318,6 @@ class MessageState extends ChangeNotifier {
} }
} }
///////////// /////////////
/// ACN /// /// ACN ///
///////////// /////////////

View File

@ -77,7 +77,7 @@ class _GlobalSettingsViewState extends State<GlobalSettingsView> {
SwitchListTile( SwitchListTile(
title: Text(AppLocalizations.of(context).blockUnknownLabel, style: TextStyle(color: settings.current().mainTextColor())), title: Text(AppLocalizations.of(context).blockUnknownLabel, style: TextStyle(color: settings.current().mainTextColor())),
subtitle: Text(AppLocalizations.of(context).descriptionBlockUnknownConnections), subtitle: Text(AppLocalizations.of(context).descriptionBlockUnknownConnections),
value: settings.blockUnknownConnections, value: settings.blockUnknownConnections,
onChanged: (bool value) { onChanged: (bool value) {
if (value) { if (value) {
settings.forbidUnknownConnections(); settings.forbidUnknownConnections();

View File

@ -44,9 +44,9 @@ class _MessageViewState extends State<MessageView> {
} }
void _debugResetContact() { void _debugResetContact() {
Provider.of<FlwtchState>(context, listen: false).cwtch.DebugResetContact( Provider.of<FlwtchState>(context, listen: false)
Provider.of<ContactInfoState>(context, listen: false).profileOnion, .cwtch
Provider.of<ContactInfoState>(context, listen: false).onion); .DebugResetContact(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion);
} }
void _pushContactSettings() { void _pushContactSettings() {
@ -69,13 +69,11 @@ class _MessageViewState extends State<MessageView> {
void _sendMessage([String ignoredParam]) { void _sendMessage([String ignoredParam]) {
ChatMessage cm = new ChatMessage(o: 1, d: ctrlrCompose.value.text); ChatMessage cm = new ChatMessage(o: 1, d: ctrlrCompose.value.text);
Provider.of<FlwtchState>(context, listen:false).cwtch.SendMessage( Provider.of<FlwtchState>(context, listen: false)
Provider.of<ContactInfoState>(context, listen:false).profileOnion, .cwtch
Provider.of<ContactInfoState>(context, listen:false).onion, .SendMessage(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion, jsonEncode(cm));
jsonEncode(cm));
ctrlrCompose.clear(); ctrlrCompose.clear();
focusNode.requestFocus(); Provider.of<ContactInfoState>(context, listen: false).totalMessages++;
Provider.of<ContactInfoState>(context, listen:false).totalMessages++;
} }
Widget _buildComposeBox() { Widget _buildComposeBox() {
@ -85,45 +83,52 @@ class _MessageViewState extends State<MessageView> {
padding: EdgeInsets.all(8.0), padding: EdgeInsets.all(8.0),
child: Row( child: Row(
children: <Widget>[ children: <Widget>[
Expanded(child: TextField( Expanded(
controller: ctrlrCompose, child: TextField(
focusNode: focusNode, controller: ctrlrCompose,
textInputAction: TextInputAction.send, focusNode: focusNode,
onSubmitted: _sendMessage, textInputAction: TextInputAction.send,
onSubmitted: _sendMessage,
)), )),
SizedBox( SizedBox(
width: 90, width: 90,
height: 80, height: 80,
child: Column(children: <Widget>[ child: Column(children: <Widget>[
Padding(padding: EdgeInsets.fromLTRB(2, 2, 2, 2), child: ElevatedButton( Padding(
child: Icon(Icons.send, color: Opaque.current().mainTextColor()), padding: EdgeInsets.fromLTRB(2, 2, 2, 2),
style: ButtonStyle( child: ElevatedButton(
fixedSize: MaterialStateProperty.all(Size(86, 40)), child: Icon(Icons.send, color: Opaque.current().mainTextColor()),
backgroundColor: MaterialStateProperty.all(Opaque.current().defaultButtonColor()), style: ButtonStyle(
), fixedSize: MaterialStateProperty.all(Size(86, 40)),
onPressed: _sendMessage, backgroundColor: MaterialStateProperty.all(Opaque.current().defaultButtonColor()),
)), ),
onPressed: _sendMessage,
)),
Row(children: <Widget>[ Row(children: <Widget>[
Padding(padding: EdgeInsets.all(2), child: SizedBox( Padding(
width: 41, padding: EdgeInsets.all(2),
child: ElevatedButton( child: SizedBox(
child: Icon(Icons.emoji_emotions_outlined, color: Opaque.current().mainTextColor()), width: 41,
style: ButtonStyle( child: ElevatedButton(
fixedSize: MaterialStateProperty.all(Size(41, 40)), child: Icon(Icons.emoji_emotions_outlined, color: Opaque.current().mainTextColor()),
backgroundColor: MaterialStateProperty.all(Opaque.current().defaultButtonColor()), style: ButtonStyle(
), fixedSize: MaterialStateProperty.all(Size(41, 40)),
onPressed: placeHolder, backgroundColor: MaterialStateProperty.all(Opaque.current().defaultButtonColor()),
))), ),
Padding(padding: EdgeInsets.all(2), child: SizedBox( onPressed: placeHolder,
width: 41, ))),
child: ElevatedButton( Padding(
child: Icon(Icons.attach_file, color: Opaque.current().mainTextColor()), padding: EdgeInsets.all(2),
style: ButtonStyle( child: SizedBox(
fixedSize: MaterialStateProperty.all(Size(41, 40)), width: 41,
backgroundColor: MaterialStateProperty.all(Opaque.current().defaultButtonColor()), child: ElevatedButton(
), child: Icon(Icons.attach_file, color: Opaque.current().mainTextColor()),
onPressed: placeHolder, style: ButtonStyle(
))), fixedSize: MaterialStateProperty.all(Size(41, 40)),
backgroundColor: MaterialStateProperty.all(Opaque.current().defaultButtonColor()),
),
onPressed: placeHolder,
))),
]) ])
]), ]),
), ),

View File

@ -1,6 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_app/settings.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_app/widgets/profilerow.dart'; import 'package:flutter_app/widgets/profilerow.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -28,6 +29,7 @@ class _ProfileMgrViewState extends State<ProfileMgrView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Provider.of<Settings>(context).theme.backgroundMainColor(),
appBar: AppBar( appBar: AppBar(
title: Text(AppLocalizations.of(context).titleManageProfiles), title: Text(AppLocalizations.of(context).titleManageProfiles),
actions: [ actions: [

View File

@ -1,10 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_app/views/messageview.dart'; import 'package:flutter_app/views/messageview.dart';
import 'package:flutter_app/widgets/profileimage.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../main.dart'; import '../main.dart';
import '../model.dart'; import '../model.dart';
import '../opaque.dart'; import '../opaque.dart';
import '../settings.dart';
class ContactRow extends StatefulWidget { class ContactRow extends StatefulWidget {
@override @override
@ -15,45 +17,58 @@ class _ContactRowState extends State<ContactRow> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var contact = Provider.of<ContactInfoState>(context); var contact = Provider.of<ContactInfoState>(context);
return ListTile( return Card(
leading: SizedBox( clipBehavior: Clip.antiAlias,
width: 60, child: InkWell(
height: 60, child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
child: ClipOval( Padding(
child: SizedBox( padding: const EdgeInsets.all(2.0), //border size
width: 60, child: ProfileImage(
height: 60, diameter: 64.0,
child: Container( imagePath: contact.imagePath,
color: Colors.white, border: contact.status == "Authenticated" ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor()),
width: 60, ),
height: 60, Expanded(
child: Image( child: Column(
image: AssetImage("assets/" + contact.imagePath), children: [
width: 50, Text(
height: 50, contact.nickname, //(contact.isInvitation ? "invite " : "non-invite ") + (contact.isBlocked ? "blokt" : "nonblokt"),//
))), style: Provider.of<FlwtchState>(context).biggerFont,
), softWrap: true,
), overflow: TextOverflow.visible,
trailing: contact.isInvitation != null && contact.isInvitation ),
? Wrap(direction: Axis.vertical, children: <Widget>[ Text(contact.status),
IconButton(padding: EdgeInsets.zero, iconSize: 16, icon: Icon(Icons.favorite, color: Opaque.current().mainTextColor()), onPressed: _btnApprove,), ],
IconButton(padding: EdgeInsets.zero, iconSize: 16, icon: Icon(Icons.delete, color: Opaque.current().mainTextColor()), onPressed: _btnReject,)]) )),
: Text(contact.unreadMessages.toString()), //(nb: Icons.create is a pencil and we use it for "edit", not create) Padding(
title: Text( padding: const EdgeInsets.all(5.0),
contact.nickname, child: contact.isInvitation != null && contact.isInvitation
style: Provider.of<FlwtchState>(context).biggerFont, ? Wrap(direction: Axis.vertical, children: <Widget>[
), IconButton(
subtitle: Text(contact.status), padding: EdgeInsets.zero,
onTap: () { iconSize: 16,
setState(() { icon: Icon(Icons.favorite, color: Opaque.current().mainTextColor()),
var flwtch = Provider.of<FlwtchState>(context, listen: false); onPressed: _btnApprove,
flwtch.setState(() => flwtch.selectedConversation = contact.onion); ),
IconButton(
// case 2/3 handled by Double/TripleColumnView respectively padding: EdgeInsets.zero,
if (flwtch.columns.length == 1) _pushMessageView(contact.onion); iconSize: 16,
}); icon: Icon(Icons.delete, color: Opaque.current().mainTextColor()),
}, onPressed: _btnReject,
); )
])
: Text(contact.unreadMessages.toString()), //(nb: Icons.create is a pencil and we use it for "edit", not create)
),
]),
onTap: () {
setState(() {
var flwtch = Provider.of<FlwtchState>(context, listen: false);
flwtch.setState(() => flwtch.selectedConversation = contact.onion);
// case 2/3 handled by Double/TripleColumnView respectively
if (flwtch.columns.length == 1) _pushMessageView(contact.onion);
});
},
));
} }
void _pushMessageView(String handle) { void _pushMessageView(String handle) {
@ -72,16 +87,15 @@ class _ContactRowState extends State<ContactRow> {
); );
} }
void _btnApprove() { void _btnApprove() {
Provider.of<FlwtchState>(context, listen: false).cwtch.AcceptContact( Provider.of<FlwtchState>(context, listen: false)
Provider.of<ContactInfoState>(context, listen: false).profileOnion, .cwtch
Provider.of<ContactInfoState>(context, listen: false).onion); .AcceptContact(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion);
} }
void _btnReject() { void _btnReject() {
Provider.of<FlwtchState>(context, listen: false).cwtch.BlockContact( Provider.of<FlwtchState>(context, listen: false)
Provider.of<ContactInfoState>(context, listen: false).profileOnion, .cwtch
Provider.of<ContactInfoState>(context, listen: false).onion); .BlockContact(Provider.of<ContactInfoState>(context, listen: false).profileOnion, Provider.of<ContactInfoState>(context, listen: false).onion);
} }
} }

View File

@ -29,7 +29,9 @@ class _MessageBubbleState extends State<MessageBubble> {
subtitle: Row( subtitle: Row(
children: [ children: [
Text(Provider.of<MessageState>(context).timestamp), Text(Provider.of<MessageState>(context).timestamp),
Provider.of<MessageState>(context).ackd ? Icon(Icons.check_circle_outline, color: Opaque.current().mainTextColor()) : Icon(Icons.hourglass_bottom_outlined, color: Opaque.current().mainTextColor()) Provider.of<MessageState>(context).ackd
? Icon(Icons.check_circle_outline, color: Opaque.current().mainTextColor())
: Icon(Icons.hourglass_bottom_outlined, color: Opaque.current().mainTextColor())
], ],
), ),
), ),

View File

@ -40,19 +40,22 @@ class _MessageListState extends State<MessageList> {
@override @override
Widget build(BuildContext outerContext) { Widget build(BuildContext outerContext) {
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToBottom(false)); WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToBottom(false));
return Card(child: Scrollbar( return Card(
child: Scrollbar(
isAlwaysShown: true, isAlwaysShown: true,
controller: ctrlr1, controller: ctrlr1,
child: ListView.builder( child: ListView.builder(
controller: ctrlr1, controller: ctrlr1,
itemCount: Provider.of<ContactInfoState>(context).totalMessages, itemCount: Provider.of<ContactInfoState>(context).totalMessages,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return ChangeNotifierProvider(create: (_) => MessageState( return ChangeNotifierProvider(
context: context, create: (_) => MessageState(
profileOnion: Provider.of<ProfileInfoState>(outerContext).onion, context: context,
contactHandle: Provider.of<ContactInfoState>(outerContext).onion, profileOnion: Provider.of<ProfileInfoState>(outerContext).onion,
messageIndex: index, contactHandle: Provider.of<ContactInfoState>(outerContext).onion,
), child: MessageBubble()); messageIndex: index,
),
child: MessageBubble());
}, },
), ),
)); ));

View File

@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:flutter_app/opaque.dart';
import 'package:provider/provider.dart';
import '../settings.dart';
class ProfileImage extends StatefulWidget {
ProfileImage({this.imagePath, this.diameter, this.border});
final double diameter;
final String imagePath;
final Color border;
@override
_ProfileImageState createState() => _ProfileImageState();
}
class _ProfileImageState extends State<ProfileImage> {
@override
Widget build(BuildContext context) {
return ClipOval(
clipBehavior: Clip.antiAlias,
child: Container(
width: widget.diameter,
height: widget.diameter,
color: widget.border,
child: Padding(
padding: const EdgeInsets.all(2.0), //border size
child: ClipOval(
clipBehavior: Clip.antiAlias,
child: Image(
image: AssetImage("assets/" + widget.imagePath),
filterQuality: FilterQuality.medium,
// We need some theme specific blending here...we might want to consider making this a theme level attribute
colorBlendMode: Provider.of<Settings>(context).theme == Opaque.dark ? BlendMode.softLight : BlendMode.darken,
color: Provider.of<Settings>(context).theme.backgroundHilightElementColor(),
isAntiAlias: true,
width: widget.diameter,
height: widget.diameter,
))),
));
}
}

View File

@ -3,6 +3,7 @@ import 'package:flutter/rendering.dart';
import 'package:flutter_app/views/addeditprofileview.dart'; import 'package:flutter_app/views/addeditprofileview.dart';
import 'package:flutter_app/views/contactsview.dart'; import 'package:flutter_app/views/contactsview.dart';
import 'package:flutter_app/views/doublecolview.dart'; import 'package:flutter_app/views/doublecolview.dart';
import 'package:flutter_app/widgets/profileimage.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -19,63 +20,66 @@ class _ProfileRowState extends State<ProfileRow> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var profile = Provider.of<ProfileInfoState>(context); var profile = Provider.of<ProfileInfoState>(context);
return ListTile( return Card(
leading: SizedBox( clipBehavior: Clip.antiAlias,
width: 60, child: InkWell(
height: 60, child: Row(
child: ClipOval( mainAxisAlignment: MainAxisAlignment.spaceBetween,
child: SizedBox( children: [
width: 60, Padding(
height: 60, padding: const EdgeInsets.all(2.0), //border size
child: Container( child: ProfileImage(
color: Colors.white, diameter: 64.0,
width: 60, imagePath: profile.imagePath,
height: 60, border: profile.isOnline ? Provider.of<Settings>(context).theme.portraitOnlineBorderColor() : Provider.of<Settings>(context).theme.portraitOfflineBorderColor())),
child: Image( Expanded(
excludeFromSemantics: true, child: Column(
image: AssetImage("assets/" + profile.imagePath), children: [
width: 50, Text(
height: 50, profile.nickname,
))), semanticsLabel: profile.nickname,
), style: Provider.of<FlwtchState>(context).biggerFont,
), softWrap: true,
overflow: TextOverflow.ellipsis,
),
ExcludeSemantics(
child: Text(
profile.onion,
softWrap: true,
overflow: TextOverflow.ellipsis,
))
],
)),
IconButton(
enableFeedback: true,
tooltip: AppLocalizations.of(context).editProfile + " " + profile.nickname,
icon: Icon(Icons.create, color: Provider.of<Settings>(context).current().mainTextColor()),
onPressed: () {
_pushAddEditProfile(onion: profile.onion, displayName: profile.nickname, profileImage: profile.imagePath);
},
)
],
),
onTap: () {
setState(() {
var flwtch = Provider.of<FlwtchState>(context, listen: false);
flwtch.cwtch.SelectProfile(profile.onion);
flwtch.setState(() {
flwtch.selectedProfile = profile;
flwtch.selectedConversation = "";
});
title: Text( switch (flwtch.columns.length) {
profile.nickname, case 1:
semanticsLabel: profile.nickname, _pushContactList(profile, false);
style: Provider.of<FlwtchState>(context).biggerFont, break;
), case 2:
subtitle: ExcludeSemantics(child: Text(profile.onion)), _pushContactList(profile, true);
break;
trailing: IconButton( } // case 3: handled by TripleColumnView
enableFeedback: true, });
tooltip: AppLocalizations.of(context).editProfile + " " + profile.nickname, },
icon: Icon(Icons.create, color: Provider.of<Settings>(context).current().mainTextColor()), ));
onPressed: () {
_pushAddEditProfile(onion: profile.onion, displayName: profile.nickname, profileImage: profile.imagePath);
},
), //(nb: Icons.create is a pencil and we use it for "edit", not create)
onTap: () {
setState(() {
var flwtch = Provider.of<FlwtchState>(context, listen: false);
flwtch.cwtch.SelectProfile(profile.onion);
flwtch.setState(() {
flwtch.selectedProfile = profile;
flwtch.selectedConversation = "";
});
switch (flwtch.columns.length) {
case 1:
_pushContactList(profile, false);
break;
case 2:
_pushContactList(profile, true);
break;
} // case 3: handled by TripleColumnView
});
},
);
} }
void _pushContactList(ProfileInfoState profile, bool includeDoublePane) { void _pushContactList(ProfileInfoState profile, bool includeDoublePane) {