...
 
Commits (5)
......@@ -20,7 +20,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/redirect.dart';
import 'package:provider/provider.dart';
......@@ -32,6 +31,7 @@ import 'resources/theme.dart';
import 'section/main/chat/page.dart';
import 'section/main/chat/image/page.dart';
import 'section/main/chat/settings/page.dart';
import 'section/main/chats/models/chat_overview.dart';
import 'section/main/chats/page.dart';
import 'section/main/chats/create/group/details_page.dart';
import 'section/main/chats/create/group/members_page.dart';
......@@ -67,7 +67,7 @@ final routes = {
),
Routes.chats: (Object arguments) => MaterialPageRoute(
settings: RouteSettings(name: Routes.chats),
builder: (context) => arguments is Room
builder: (context) => arguments is Chat
? ChatPage.withBloc(arguments)
: ChatsPage.withBloc(),
),
......
......@@ -172,7 +172,7 @@ class NotificationsBloc extends Bloc<NotificationsEvent, NotificationsState> {
await plugin.show(
event.id.hashCode,
nameOf(room),
room.getDisplayName(),
message.text,
NotificationDetails(
AndroidNotificationDetails(
......@@ -186,7 +186,8 @@ class NotificationsBloc extends Bloc<NotificationsEvent, NotificationsState> {
style: AndroidNotificationStyle.Messaging,
styleInformation: MessagingStyleInformation(
senderPerson,
conversationTitle: !room.isDirect ? await nameOf(room) : null,
conversationTitle:
!room.isDirect ? await room.getDisplayName() : null,
groupConversation: room.isDirect,
messages: [message],
),
......
......@@ -48,8 +48,6 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
});
}
List<Type> get _ignoredEvents => ignoredEventsOf(room, isOverview: false);
LocalUser get me => matrix.user;
// TODO: Move to separate bloc
......@@ -165,7 +163,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
// Don't show creation events in rooms that are replacements
shouldIgnore |= event is RoomCreationEvent && room.isReplacement;
if (_ignoredEvents.contains(event.runtimeType) || shouldIgnore) {
if (room.ignoredEvents.contains(event.runtimeType) || shouldIgnore) {
continue;
}
......@@ -192,7 +190,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
);
}
bool endReached = false;
bool endReached;
if (event is RoomCreationEvent) {
endReached = true;
}
......@@ -207,7 +205,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
return ChatLoaded(
messages: messages,
pageCount: page + 1,
endReached: endReached,
endReached: endReached ?? false,
);
}
}
......
This diff is collapsed.
......@@ -23,6 +23,7 @@ import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/resources/localizations.dart';
import 'package:pattle/src/resources/theme.dart';
import 'package:pattle/src/section/main/chats/models/chat_overview.dart';
import 'package:pattle/src/section/main/widgets/chat_name.dart';
import 'package:pattle/src/section/main/widgets/user_item.dart';
......@@ -33,14 +34,14 @@ import '../../../../util/room.dart';
import 'bloc.dart';
class ChatSettingsPage extends StatefulWidget {
final Room room;
final Chat chat;
ChatSettingsPage._(this.room);
ChatSettingsPage._(this.chat);
static Widget withBloc(Room room) {
static Widget withBloc(Chat chat) {
return BlocProvider<ChatSettingsBloc>(
create: (c) => ChatSettingsBloc(Matrix.of(c), room),
child: ChatSettingsPage._(room),
create: (c) => ChatSettingsBloc(Matrix.of(c), chat.room),
child: ChatSettingsPage._(chat),
);
}
......@@ -49,7 +50,7 @@ class ChatSettingsPage extends StatefulWidget {
}
class _ChatSettingsPageState extends State<ChatSettingsPage> {
Room get room => widget.room;
Room get room => widget.chat.room;
@override
void didChangeDependencies() {
......@@ -67,15 +68,15 @@ class _ChatSettingsPageState extends State<ChatSettingsPage> {
),
body: NestedScrollView(
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
final url = avatarUrlOf(room)?.toDownloadString(context);
final url =
widget.chat.room.displayAvatarUrl?.toDownloadString(context);
return <Widget>[
SliverAppBar(
expandedHeight: 128.0,
floating: false,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: ChatName(
room: room,
title: DefaultTextStyle(
style: TextStyle(
shadows: [
Shadow(
......@@ -84,6 +85,9 @@ class _ChatSettingsPageState extends State<ChatSettingsPage> {
)
],
),
child: ChatName(
chat: widget.chat,
),
),
background: url != null
? CachedNetworkImage(
......
// Copyright (C) 2020 Wilko Manger
//
// This file is part of Pattle.
//
// Pattle is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Pattle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Pattle. If not, see <https://www.gnu.org/licenses/>.
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:pattle/src/section/main/chat/widgets/bubble/message.dart';
import 'package:shimmer/shimmer.dart';
class LoadingContent extends StatefulWidget {
@override
State<StatefulWidget> createState() => _LoadingContentState();
}
class _LoadingContentState extends State<LoadingContent> {
static const _minWidth = 164;
static const _maxWidth = 256;
static const _minHeight = 56;
static const _maxHeight = 95;
double _width, _height;
@override
void initState() {
super.initState();
final random = Random();
_width = _minWidth + random.nextInt(_maxWidth - _minWidth).toDouble();
_height = _minHeight + random.nextInt(_maxHeight - _minHeight).toDouble();
}
@override
Widget build(BuildContext context) {
final bubble = MessageBubble.of(context);
return Shimmer.fromColors(
baseColor: bubble.color,
highlightColor: Colors.grey[200],
child: Container(
width: _width,
height: _height,
decoration: BoxDecoration(
borderRadius: bubble.borderRadius,
color: bubble.color,
),
),
);
}
}
......@@ -18,6 +18,7 @@
import 'package:flutter/material.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/resources/theme.dart';
import 'package:pattle/src/section/main/chat/widgets/bubble/message/content/loading.dart';
import 'package:pattle/src/section/main/models/chat_message.dart';
import 'package:pattle/src/section/main/widgets/message_state.dart';
import 'package:pattle/src/util/color.dart';
......@@ -44,6 +45,8 @@ class MessageBubble extends StatelessWidget {
final BorderRadius borderRadius;
final Color color;
final Widget child;
final EdgeInsets contentPadding = EdgeInsets.all(8);
......@@ -57,21 +60,23 @@ class MessageBubble extends StatelessWidget {
MessageBubble._({
@required this.message,
@required this.previousMessage,
@required this.nextMessage,
this.previousMessage,
this.nextMessage,
@required this.isStartOfGroup,
@required this.isEndOfGroup,
this.reply,
@required this.borderRadius,
@required this.child,
this.color,
});
factory MessageBubble({
ChatMessage message,
@required ChatMessage message,
ChatMessage previousMessage,
ChatMessage nextMessage,
ChatMessage reply,
Widget child,
Color color,
@required Widget child,
}) {
final isStartOfGroup = _isStartOfGroup(message, previousMessage, reply);
final isEndOfGroup = _isEndofGroup(message, nextMessage, reply);
......@@ -84,6 +89,7 @@ class MessageBubble extends StatelessWidget {
isEndOfGroup: isEndOfGroup,
reply: reply,
borderRadius: _borderRadius(message, isEndOfGroup, isStartOfGroup),
color: color,
child: child,
);
}
......@@ -116,6 +122,34 @@ class MessageBubble extends StatelessWidget {
);
}
factory MessageBubble.loading({@required Room room, bool isMine = false}) {
return MessageBubble(
color: Colors.grey[300],
message: ChatMessage(
room,
TextMessageEvent(
RoomEventArgs(
id: EventId('1234'),
sender: User(
id: UserId('@wilko:pattle.im'),
state: UserState(
roomId: RoomId('!343432:pattle.im'),
displayName: 'Wilko',
since: DateTime.now(),
),
),
time: DateTime.now(),
),
content: TextMessage(
body: 'Blabla',
),
),
isMine: isMine,
),
child: LoadingContent(),
);
}
static bool _isStartOfGroup(
ChatMessage message,
ChatMessage previousMessage,
......@@ -215,17 +249,18 @@ class MessageBubble extends StatelessWidget {
@override
Widget build(BuildContext context) {
final color = message.isMine
? themed(
context,
light: LightColors.red[450],
dark: LightColors.red[700],
)
: themed(
context,
light: Colors.white,
dark: Colors.grey[800],
);
final color = this.color ??
(message.isMine
? themed(
context,
light: LightColors.red[450],
dark: LightColors.red[700],
)
: themed(
context,
light: Colors.white,
dark: Colors.grey[800],
));
final border = RoundedRectangleBorder(borderRadius: borderRadius);
......
......@@ -34,12 +34,12 @@ class ChatsBloc extends Bloc<ChatsEvent, ChatsState> {
ChatsBloc(this._matrix);
Future<List<ChatOverview>> _getChats() async {
Future<List<Chat>> _getChats() async {
final me = _matrix.user;
await me.sync.first;
final chats = List<ChatOverview>();
final chats = List<Chat>();
// Get all rooms and push them as a single list
for (Room room in await me.rooms.get()) {
......@@ -48,12 +48,10 @@ class ChatsBloc extends Bloc<ChatsEvent, ChatsState> {
continue;
}
final ignoredEvents = ignoredEventsOf(room, isOverview: true);
// TODO: Add optional filter argument to up to call
final latestEvent =
(await room.timeline.get(upTo: 10, allowRemote: false)).firstWhere(
(event) => !ignoredEvents.contains(event.runtimeType),
(event) => !room.ignoredEvents.contains(event.runtimeType),
orElse: () => null,
);
......@@ -76,9 +74,10 @@ class ChatsBloc extends Bloc<ChatsEvent, ChatsState> {
latestEventForSorting = latestEvent;
}
final chat = ChatOverview(
final chat = Chat(
room: room,
name: room.name,
name: await room.getDisplayName(),
isJustYou: room.members.count == 1,
latestMessage: latestEvent != null
? ChatMessage(
room,
......
......@@ -20,7 +20,7 @@ import 'package:meta/meta.dart';
import 'package:pattle/src/section/main/models/chat_message.dart';
/// Chat overview used in the 'chats' page.
class ChatOverview {
class Chat {
final Room room;
final String name;
......@@ -28,10 +28,13 @@ class ChatOverview {
final ChatMessage latestMessage;
final ChatMessage latestMessageForSorting;
ChatOverview({
final bool isJustYou;
Chat({
@required this.room,
@required this.name,
@required this.latestMessage,
@required this.latestMessageForSorting,
this.isJustYou = false,
});
}
......@@ -10,8 +10,8 @@ abstract class ChatsState extends Equatable {
class ChatsLoading extends ChatsState {}
class ChatsLoaded extends ChatsState {
final List<ChatOverview> personal;
final List<ChatOverview> public;
final List<Chat> personal;
final List<Chat> public;
ChatsLoaded({@required this.personal, @required this.public});
......
......@@ -33,7 +33,7 @@ class ChatAvatar extends StatelessWidget {
@override
Widget build(BuildContext context) {
final avatarUrl = avatarUrlOf(room);
final avatarUrl = room.displayAvatarUrl;
if (avatarUrl != null) {
return Container(
width: 48,
......
......@@ -25,7 +25,7 @@ import 'chat_avatar.dart';
import 'subtitle.dart';
class ChatOverviewList extends StatefulWidget {
final List<ChatOverview> chats;
final List<Chat> chats;
const ChatOverviewList({Key key, this.chats}) : super(key: key);
......@@ -51,7 +51,7 @@ class ChatOverviewListState extends State<ChatOverviewList> {
);
}
Widget _buildChatOverview(ChatOverview chat) {
Widget _buildChatOverview(Chat chat) {
final time = formatAsListItem(context, chat.latestMessage?.event?.time);
return ListTile(
......@@ -61,7 +61,7 @@ class ChatOverviewListState extends State<ChatOverviewList> {
children: <Widget>[
Expanded(
child: ChatName(
room: chat.room,
chat: chat,
),
),
Text(
......@@ -75,7 +75,7 @@ class ChatOverviewListState extends State<ChatOverviewList> {
),
dense: false,
onTap: () {
Navigator.pushNamed(context, Routes.chats, arguments: chat.room);
Navigator.pushNamed(context, Routes.chats, arguments: chat);
},
leading: ChatAvatar(room: chat.room),
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 4),
......
......@@ -35,12 +35,12 @@ import 'typing_subtitle.dart';
import 'unsupported_subtitle.dart';
class Subtitle extends StatelessWidget {
final ChatOverview chat;
final Chat chat;
final Widget child;
const Subtitle({Key key, this.chat, this.child}) : super(key: key);
static Widget withContent(ChatOverview chat) {
static Widget withContent(Chat chat) {
Widget content;
// TODO: typingUsers should not contain nulls
......
......@@ -16,43 +16,24 @@
// along with Pattle. If not, see <https://www.gnu.org/licenses/>.
import 'package:flutter/material.dart';
import 'package:future_or_builder/future_or_builder.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:meta/meta.dart';
import '../../../util/room.dart';
import 'package:pattle/src/resources/localizations.dart';
import 'package:pattle/src/section/main/chats/models/chat_overview.dart';
class ChatName extends StatelessWidget {
final Room room;
final TextStyle style;
ChatName({
@required this.room,
this.style = const TextStyle(),
});
final Chat chat;
TextStyle _textStyle() => style.copyWith(
fontWeight: FontWeight.w600,
);
ChatName({@required this.chat});
@override
Widget build(BuildContext context) {
return FutureOrBuilder<String>(
futureOr: nameOf(room, context),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.hasData) {
final name = snapshot.data;
return Text(
name,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: _textStyle(),
);
}
return Container();
},
return Text(
chat.isJustYou ? l(context).you : chat.name,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: TextStyle(
fontWeight: FontWeight.w600,
),
);
}
}
......@@ -24,74 +24,66 @@ import 'package:pattle/src/resources/localizations.dart';
import '../matrix.dart';
import '../util/user.dart';
Uri avatarUrlOf(Room room) =>
room.avatarUrl ??
(room.isDirect ? room.directUser.avatarUrl : room.avatarUrl);
extension RoomExtension on Room {
Uri get displayAvatarUrl =>
avatarUrl ?? (isDirect ? directUser.avatarUrl : avatarUrl);
FutureOr<String> nameOf(Room room, [BuildContext context]) {
if (room.name != null) {
return room.name;
}
if (room.isDirect) {
return room.directUser.getDisplayName(context);
}
String calculateName(Iterable<User> members) {
var name = '';
if (members != null) {
if (members.length == 1 && context != null) {
return l(context).you;
// TODO: Check for aliases (public chats)
} else {
final nonMeMembers = members
.where((user) => context != null && user != Matrix.of(context).user)
.toList(growable: false);
var i = 0;
for (User member in nonMeMembers) {
if (i > 4) {
name += ' ${l(context).andOthers}';
break;
}
FutureOr<String> getDisplayName([BuildContext context]) {
if (name != null) {
return name;
}
name += member.getDisplayName(context);
if (isDirect) {
return directUser.getDisplayName(context);
}
if (i != nonMeMembers.length - 1) {
name += ', ';
String calculateName(Iterable<User> members) {
var name = '';
if (members != null) {
if (members.length == 1 && context != null) {
return l(context).you;
// TODO: Check for aliases (public chats)
} else {
final nonMeMembers = members
.where(
(user) => context != null && user != Matrix.of(context).user)
.toList(growable: false);
var i = 0;
for (User member in nonMeMembers) {
if (i > 4) {
name += ' ${l(context).andOthers}';
break;
}
name += member.getDisplayName(context);
if (i != nonMeMembers.length - 1) {
name += ', ';
}
i++;
}
i++;
}
} else {
return id.toString();
}
} else {
return room.id.toString();
}
return name.isNotEmpty ? name : room.id.toString();
}
final futureOrMembers = room.members.get(upTo: 6);
if (futureOrMembers is Future<Iterable<User>>) {
return futureOrMembers.then(calculateName);
} else {
return calculateName(futureOrMembers);
}
}
List<Type> ignoredEventsOf(Room room, {@required bool isOverview}) {
List<Type> ignored = [
RedactionEvent,
AvatarChangeEvent,
];
if (isOverview) {
ignored.add(DisplayNameChangeEvent);
}
return name.isNotEmpty ? name : id.toString();
}
if (room.isDirect) {
ignored.add(RoomCreationEvent);
final futureOrMembers = members.get(upTo: 6);
if (futureOrMembers is Future<Iterable<User>>) {
return futureOrMembers.then(calculateName);
} else {
return calculateName(futureOrMembers);
}
}
return ignored;
List<Type> get ignoredEvents => [
RedactionEvent,
AvatarChangeEvent,
DisplayNameChangeEvent,
if (isDirect) RoomCreationEvent,
];
}
......@@ -200,13 +200,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
future_or_builder:
dependency: "direct main"
description:
name: future_or_builder
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
html:
dependency: transitive
description:
......@@ -430,6 +423,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.5.4+3"
shimmer:
dependency: "direct main"
description:
name: shimmer
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
sky_engine:
dependency: transitive
description: flutter
......
......@@ -23,8 +23,6 @@ dependencies:
cached_network_image: ^2.0.0-rc.1
flutter_cache_manager: ^1.1.3
future_or_builder: ^1.0.3
sqflite: ^1.1.6
async: ^2.3.0
......@@ -62,6 +60,8 @@ dependencies:
mdi: ^0.2.1
shimmer: ^1.1.0
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.2
......