Improve repo structure (#22)

* app updated

* readme updated

* rename base folder

* update chatty's readme

* move apps to packages dir

* update repo overview

Co-authored-by: diegoveloper <[email protected]>
This commit is contained in:
Neevash Ramdial (Nash)
2021-03-10 15:18:00 -04:00
committed by GitHub
co-authored by diegoveloper
parent bd6bb66279
commit 030c3eb428
290 changed files with 3804 additions and 4 deletions
@@ -0,0 +1,7 @@
import 'package:stream_chatter/domain/models/auth_user.dart';
abstract class AuthRepository {
Future<AuthUser> getAuthUser();
Future<AuthUser> signIn();
Future<void> logout();
}
@@ -0,0 +1,5 @@
import 'dart:io';
abstract class ImagePickerRepository {
Future<File> pickImage();
}
@@ -0,0 +1,21 @@
import 'package:stream_chatter/data/auth_repository.dart';
import 'package:stream_chatter/domain/models/auth_user.dart';
class AuthLocalImpl extends AuthRepository {
@override
Future<AuthUser> getAuthUser() async {
await Future.delayed(const Duration(seconds: 2));
return AuthUser('diego');
}
@override
Future<AuthUser> signIn() async {
await Future.delayed(const Duration(seconds: 2));
return AuthUser('diego');
}
@override
Future<void> logout() async {
return;
}
}
@@ -0,0 +1,12 @@
import 'dart:io';
import 'package:image_picker/image_picker.dart';
import 'package:stream_chatter/data/image_picker_repository.dart';
class ImagePickerImpl extends ImagePickerRepository {
@override
Future<File> pickImage() async {
final picker = ImagePicker();
final pickedFile = await picker.getImage(source: ImageSource.gallery, maxWidth: 400);
return File(pickedFile.path);
}
}
@@ -0,0 +1,15 @@
import 'package:stream_chatter/data/persistent_storage_repository.dart';
class PersistentStorageLocalImpl extends PersistentStorageRepository {
@override
Future<bool> isDarkMode() async {
await Future.delayed(const Duration(milliseconds: 50));
return false;
}
@override
Future<void> updateDarkMode(bool isDarkMode) async {
await Future.delayed(const Duration(milliseconds: 50));
return;
}
}
@@ -0,0 +1,86 @@
import 'package:stream_chatter/data/stream_api_repository.dart';
import 'package:stream_chatter/domain/models/chat_user.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class StreamApiLocalImpl extends StreamApiRepository {
StreamApiLocalImpl(this._client);
final StreamChatClient _client;
@override
Future<ChatUser> connectUser(ChatUser user, String token) async {
Map<String, dynamic> extraData = {};
if (user.image != null) {
extraData['image'] = user.image;
}
if (user.name != null) {
extraData['name'] = user.name;
}
await _client.disconnect();
await _client.connectUser(
User(id: user.id, extraData: extraData),
token,
);
return user;
}
@override
Future<List<ChatUser>> getChatUsers() async {
final result = await _client.queryUsers();
final chatUsers = result.users
.where((element) => element.id != _client.state.user.id)
.map(
(e) => ChatUser(
id: e.id,
name: e.name,
image: e.extraData['image'],
),
)
.toList();
return chatUsers;
}
@override
Future<String> getToken(String userId) async {
return _client.devToken(userId);
}
@override
Future<Channel> createGroupChat(String channelId, String name, List<String> members, {String image}) async {
final channel = _client.channel('messaging', id: channelId, extraData: {
'name': name,
'image': image,
'members': [_client.state.user.id, ...members],
});
await channel.watch();
return channel;
}
@override
Future<Channel> createSimpleChat(String friendId) async {
final channel =
_client.channel('messaging', id: '${_client.state.user.id.hashCode}${friendId.hashCode}', extraData: {
'members': [
friendId,
_client.state.user.id,
],
});
await channel.watch();
return channel;
}
@override
Future<void> logout() {
return _client.disconnect();
}
@override
Future<bool> connectIfExist(String userId) async {
final token = await getToken(userId);
await _client.connectUser(
User(id: userId),
token,
);
return _client.state.user.name != null && _client.state.user.name != userId;
}
}
@@ -0,0 +1,10 @@
import 'dart:io';
import 'package:stream_chatter/data/upload_storage_repository.dart';
class UploadStorageLocalImpl extends UploadStorageRepository {
@override
Future<String> uploadPhoto(File file, String path) async {
return 'https://lh3.googleusercontent.com/a-/AOh14GjhqGZ-V7tNXS1pOIp9vbBij4OS9JbzxXgxgy1t=s600-k-no-rp-mo';
}
}
@@ -0,0 +1,4 @@
abstract class PersistentStorageRepository {
Future<bool> isDarkMode();
Future<void> updateDarkMode(bool isDarkMode);
}
@@ -0,0 +1,41 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:stream_chatter/data/auth_repository.dart';
import 'package:stream_chatter/domain/models/auth_user.dart';
import 'package:google_sign_in/google_sign_in.dart';
class AuthImpl extends AuthRepository {
FirebaseAuth _auth = FirebaseAuth.instance;
@override
Future<AuthUser> getAuthUser() async {
final user = _auth.currentUser;
if (user != null) {
return AuthUser(user.uid);
}
return null;
}
@override
Future<AuthUser> signIn() async {
try {
UserCredential userCredential;
final GoogleSignInAccount googleUser = await GoogleSignIn().signIn();
final GoogleSignInAuthentication googleAuth = await googleUser.authentication;
final GoogleAuthCredential googleAuthCredential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
userCredential = await _auth.signInWithCredential(googleAuthCredential);
final user = userCredential.user;
return AuthUser(user.uid);
} catch (e) {
print(e);
throw Exception('login error');
}
}
@override
Future<void> logout() async {
return _auth.signOut();
}
}
@@ -0,0 +1,18 @@
import 'package:stream_chatter/data/persistent_storage_repository.dart';
import 'package:shared_preferences/shared_preferences.dart';
const _isDarkMode = 'isDarkMode';
class PersistentStorageImpl extends PersistentStorageRepository {
@override
Future<bool> isDarkMode() async {
final preference = await SharedPreferences.getInstance();
return preference.getBool(_isDarkMode) ?? false;
}
@override
Future<void> updateDarkMode(bool isDarkMode) async {
final preference = await SharedPreferences.getInstance();
return await preference.setBool(_isDarkMode, isDarkMode);
}
}
@@ -0,0 +1,102 @@
import 'dart:convert';
import 'package:stream_chatter/data/stream_api_repository.dart';
import 'package:stream_chatter/domain/models/chat_user.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:http/http.dart' as http;
class StreamApiImpl extends StreamApiRepository {
StreamApiImpl(this._client);
final StreamChatClient _client;
@override
Future<ChatUser> connectUser(ChatUser user, String token) async {
Map<String, dynamic> extraData = {};
if (user.image != null) {
extraData['image'] = user.image;
}
if (user.name != null) {
extraData['name'] = user.name;
}
await _client.disconnect();
await _client.connectUser(
User(id: user.id, extraData: extraData),
token,
);
return user;
}
@override
Future<List<ChatUser>> getChatUsers() async {
final result = await _client.queryUsers();
final chatUsers = result.users
.where((element) => element.id != _client.state.user.id)
.map(
(e) => ChatUser(
id: e.id,
name: e.name,
image: e.extraData['image'],
),
)
.toList();
return chatUsers;
}
@override
Future<String> getToken(String userId) async {
//TODO: use your own implementation in Production
final response = await http.post(
'your_backend_url',
body: jsonEncode(<String, String>{'id': userId}),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
);
final token = jsonDecode(response.body)['token'];
//In Development mode you can just use :
// _client.devToken(userId);
return token;
}
@override
Future<Channel> createGroupChat(String id, String name, List<String> members, {String image}) async {
final channel = _client.channel('messaging', id: id, extraData: {
'name': name,
'image': image,
'members': [_client.state.user.id, ...members],
});
await channel.watch();
return channel;
}
@override
Future<Channel> createSimpleChat(String friendId) async {
final channel =
_client.channel('messaging', id: '${_client.state.user.id.hashCode}${friendId.hashCode}', extraData: {
'members': [
friendId,
_client.state.user.id,
],
});
await channel.watch();
return channel;
}
@override
Future<void> logout() async {
return _client.disconnect();
}
@override
Future<bool> connectIfExist(String userId) async {
final token = await getToken(userId);
await _client.connectUser(
User(id: userId),
token,
);
return _client.state.user.name != null && _client.state.user.name != userId;
}
}
@@ -0,0 +1,13 @@
import 'dart:io';
import 'package:firebase_storage/firebase_storage.dart' as firebase_storage;
import 'package:stream_chatter/data/upload_storage_repository.dart';
class UploadStorageImpl extends UploadStorageRepository {
@override
Future<String> uploadPhoto(File file, String path) async {
final ref = firebase_storage.FirebaseStorage.instance.ref(path);
final uploadTask = ref.putFile(file);
await uploadTask;
return await ref.getDownloadURL();
}
}
@@ -0,0 +1,12 @@
import 'package:stream_chatter/domain/models/chat_user.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
abstract class StreamApiRepository {
Future<List<ChatUser>> getChatUsers();
Future<String> getToken(String userId);
Future<bool> connectIfExist(String userId);
Future<ChatUser> connectUser(ChatUser user, String token);
Future<Channel> createGroupChat(String channelId, String name, List<String> members, {String image});
Future<Channel> createSimpleChat(String friendId);
Future<void> logout();
}
@@ -0,0 +1,5 @@
import 'dart:io';
abstract class UploadStorageRepository {
Future<String> uploadPhoto(File file, String path);
}
+52
View File
@@ -0,0 +1,52 @@
import 'package:stream_chatter/data/auth_repository.dart';
import 'package:stream_chatter/data/image_picker_repository.dart';
import 'package:stream_chatter/data/local/image_picker_impl.dart';
import 'package:stream_chatter/data/persistent_storage_repository.dart';
import 'package:stream_chatter/data/prod/auth_impl.dart';
import 'package:stream_chatter/data/prod/persistent_storage_impl.dart';
import 'package:stream_chatter/data/prod/stream_api_impl.dart';
import 'package:stream_chatter/data/prod/upload_storage_impl.dart';
import 'package:stream_chatter/data/stream_api_repository.dart';
import 'package:stream_chatter/data/upload_storage_repository.dart';
import 'package:stream_chatter/domain/usecases/create_group_usecase.dart';
import 'package:stream_chatter/domain/usecases/login_usecase.dart';
import 'package:stream_chatter/domain/usecases/logout_usecase.dart';
import 'package:stream_chatter/domain/usecases/profile_sign_in_usecase.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
List<RepositoryProvider> buildRepositories(StreamChatClient client) {
//TODO: Here you can use your local implementations of your repositories
return [
RepositoryProvider<StreamApiRepository>(create: (_) => StreamApiImpl(client)),
RepositoryProvider<PersistentStorageRepository>(create: (_) => PersistentStorageImpl()),
RepositoryProvider<AuthRepository>(create: (_) => AuthImpl()),
RepositoryProvider<UploadStorageRepository>(create: (_) => UploadStorageImpl()),
RepositoryProvider<ImagePickerRepository>(create: (_) => ImagePickerImpl()),
RepositoryProvider<ProfileSignInUseCase>(
create: (context) => ProfileSignInUseCase(
context.read(),
context.read(),
context.read(),
),
),
RepositoryProvider<CreateGroupUseCase>(
create: (context) => CreateGroupUseCase(
context.read(),
context.read(),
),
),
RepositoryProvider<LogoutUseCase>(
create: (context) => LogoutUseCase(
context.read(),
context.read(),
),
),
RepositoryProvider<LoginUseCase>(
create: (context) => LoginUseCase(
context.read(),
context.read(),
),
),
];
}
@@ -0,0 +1,10 @@
enum AuthErrorCode {
not_auth,
not_chat_user,
}
class AuthException implements Exception {
AuthException(this.error);
final AuthErrorCode error;
}
@@ -0,0 +1,4 @@
class AuthUser {
AuthUser(this.id);
final String id;
}
@@ -0,0 +1,6 @@
class ChatUser {
const ChatUser({this.name, this.image, this.id});
final String name;
final String image;
final String id;
}
@@ -0,0 +1,38 @@
import 'dart:io';
import 'package:stream_chatter/data/stream_api_repository.dart';
import 'package:stream_chatter/data/upload_storage_repository.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:uuid/uuid.dart';
class CreateGroupInput {
CreateGroupInput({this.imageFile, this.name, this.members});
final File imageFile;
final String name;
final List<String> members;
}
class CreateGroupUseCase {
CreateGroupUseCase(
this._streamApiRepository,
this._uploadStorageRepository,
);
final UploadStorageRepository _uploadStorageRepository;
final StreamApiRepository _streamApiRepository;
Future<Channel> createGroup(CreateGroupInput input) async {
final channelId = Uuid().v4();
String image;
if (input.imageFile != null) {
image = await _uploadStorageRepository.uploadPhoto(input.imageFile, 'channels/$channelId');
}
final channel = await _streamApiRepository.createGroupChat(
channelId,
input.name,
input.members,
image: image,
);
return channel;
}
}
@@ -0,0 +1,30 @@
import 'package:stream_chatter/data/auth_repository.dart';
import 'package:stream_chatter/data/stream_api_repository.dart';
import 'package:stream_chatter/domain/exceptions/auth_exception.dart';
import 'package:stream_chatter/domain/models/auth_user.dart';
class LoginUseCase {
LoginUseCase(this.authRepository, this.streamApiRepository);
final AuthRepository authRepository;
final StreamApiRepository streamApiRepository;
Future<bool> validateLogin() async {
print('validateLogin');
final user = await authRepository.getAuthUser();
print('user: ${user?.id}');
if (user != null) {
final result = await streamApiRepository.connectIfExist(user.id);
if (result) {
return true;
} else {
throw AuthException(AuthErrorCode.not_chat_user);
}
}
throw AuthException(AuthErrorCode.not_auth);
}
Future<AuthUser> signIn() async {
return await authRepository.signIn();
}
}
@@ -0,0 +1,13 @@
import 'package:stream_chatter/data/auth_repository.dart';
import 'package:stream_chatter/data/stream_api_repository.dart';
class LogoutUseCase {
LogoutUseCase(this.streamApiRepository, this.authRepository);
final StreamApiRepository streamApiRepository;
final AuthRepository authRepository;
Future<void> logout() async {
await streamApiRepository.logout();
await authRepository.logout();
}
}
@@ -0,0 +1,34 @@
import 'dart:io';
import 'package:stream_chatter/data/auth_repository.dart';
import 'package:stream_chatter/data/stream_api_repository.dart';
import 'package:stream_chatter/data/upload_storage_repository.dart';
import 'package:stream_chatter/domain/models/chat_user.dart';
class ProfileInput {
ProfileInput({this.imageFile, this.name});
final File imageFile;
final String name;
}
class ProfileSignInUseCase {
ProfileSignInUseCase(
this._authRepository,
this._streamApiRepository,
this._uploadStorageRepository,
);
final AuthRepository _authRepository;
final UploadStorageRepository _uploadStorageRepository;
final StreamApiRepository _streamApiRepository;
Future<void> verify(ProfileInput input) async {
final auth = await _authRepository.getAuthUser();
final token = await _streamApiRepository.getToken(auth.id);
String image;
if (input.imageFile != null) {
image = await _uploadStorageRepository.uploadPhoto(input.imageFile, 'users/${auth.id}');
}
await _streamApiRepository.connectUser(ChatUser(name: input.name, id: auth.id, image: image), token);
}
}
+50
View File
@@ -0,0 +1,50 @@
import 'package:firebase_core/firebase_core.dart';
import 'package:stream_chatter/dependencies.dart';
import 'package:stream_chatter/ui/app_theme_cubit.dart';
import 'package:stream_chatter/ui/splash/splash_view.dart';
import 'package:stream_chatter/ui/themes.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final _streamChatClient = StreamChatClient('c2rynysx9x6b');
@override
Widget build(BuildContext context) {
SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]);
return MultiRepositoryProvider(
providers: buildRepositories(_streamChatClient),
child: BlocProvider(
create: (context) => AppThemeCubit(context.read())..init(),
child: BlocBuilder<AppThemeCubit, bool>(builder: (context, snapshot) {
return MaterialApp(
title: 'Stream Chatty',
home: SplashView(),
theme: snapshot ? Themes.themeDark : Themes.themeLight,
builder: (context, child) {
return StreamChat(
child: child,
client: _streamChatClient,
streamChatThemeData: StreamChatThemeData.fromTheme(Theme.of(context)).copyWith(
ownMessageTheme: MessageTheme(
messageBackgroundColor: Theme.of(context).accentColor,
messageText: TextStyle(color: Colors.white),
),
),
);
},
);
}),
),
);
}
}
+22
View File
@@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
Future pushToPage(BuildContext context, Widget widget) async {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => widget,
),
);
}
Future pushAndReplaceToPage(BuildContext context, Widget widget) async {
await Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => widget,
),
);
}
Future popAllAndPush(BuildContext context, Widget widget) async {
await Navigator.pushAndRemoveUntil(
context, MaterialPageRoute(builder: (BuildContext context) => widget), ModalRoute.withName('/'));
}
@@ -0,0 +1,21 @@
import 'package:stream_chatter/data/persistent_storage_repository.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class AppThemeCubit extends Cubit<bool> {
AppThemeCubit(this._persistentStorageRepository) : super(false);
final PersistentStorageRepository _persistentStorageRepository;
bool _isDark = false;
bool get isDark => _isDark;
Future<void> init() async {
_isDark = await _persistentStorageRepository.isDarkMode();
emit(_isDark);
}
Future<void> updateTheme(bool isDarkMode) async {
_isDark = isDarkMode;
await _persistentStorageRepository.updateDarkMode(isDarkMode);
emit(_isDark);
}
}
@@ -0,0 +1,45 @@
import 'package:flutter/material.dart';
class AvatarImageView extends StatelessWidget {
const AvatarImageView({Key key, this.onTap, this.child}) : super(key: key);
final Widget child;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(20.0),
child: Stack(
clipBehavior: Clip.none,
children: [
ClipOval(
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey[100],
),
height: 180,
width: 180,
child: child,
),
),
Positioned(
bottom: -15,
right: 0,
child: GestureDetector(
onTap: onTap,
child: CircleAvatar(
backgroundColor: Colors.white,
radius: 30,
child: Icon(
Icons.camera_alt_outlined,
color: Colors.black,
),
),
),
),
],
),
);
}
}
@@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
class InitialBackgroundView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Stack(
children: [
Positioned(
top: 20,
right: -75,
child: Image.asset(
'assets/icon-top-right.png',
height: 150,
),
),
Positioned(
bottom: -50,
right: -50,
child: Image.asset(
'assets/icon-bottom-right.png',
height: 200,
),
),
],
);
}
}
@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
class LoadingView extends StatelessWidget {
final bool isLoading;
final Widget child;
const LoadingView({
Key key,
@required this.child,
this.isLoading = false,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
child,
if (isLoading)
Container(
color: Colors.black26,
child: Center(
child: CircularProgressIndicator(),
),
),
],
),
);
}
}
@@ -0,0 +1,326 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/*Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return StreamChannel(
child: widget.channelWidget,
channel: client,
);
},
),
);
*/
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview_paint.png)
///
/// It shows the current [Channel] preview.
///
/// The widget uses a [StreamBuilder] to render the channel information image as soon as it updates.
///
/// Usually you don't use this widget as it's the default channel preview used by [ChannelListView].
///
/// The widget renders the ui based on the first ancestor of type [StreamChatTheme].
/// Modify it to change the widget appearance.
class MyChannelPreview extends StatelessWidget {
/// Function called when tapping this widget
final void Function(Channel) onTap;
/// Function called when long pressing this widget
final void Function(Channel) onLongPress;
/// Channel displayed
final Channel channel;
/// The function called when the image is tapped
final VoidCallback onImageTap;
final String heroTag;
MyChannelPreview({
@required this.channel,
Key key,
this.onTap,
this.onLongPress,
this.onImageTap,
this.heroTag,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return StreamBuilder<bool>(
stream: channel.isMutedStream,
initialData: channel.isMuted,
builder: (context, snapshot) {
return Opacity(
opacity: snapshot.data ? 0.5 : 1,
child: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 8,
),
onTap: () {
if (onTap != null) {
onTap(channel);
}
},
onLongPress: () {
if (onLongPress != null) {
onLongPress(channel);
}
},
leading: Material(
child: Hero(
tag: heroTag,
child: StreamChannel(
channel: channel,
child: ChannelImage(
onTap: onImageTap,
),
),
),
),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Flexible(
child: ChannelName(
textStyle: StreamChatTheme.of(context).channelPreviewTheme.title,
),
),
StreamBuilder<List<Member>>(
stream: channel.state.membersStream,
initialData: channel.state.members,
builder: (context, snapshot) {
if (!snapshot.hasData ||
snapshot.data.isEmpty ||
!snapshot.data.any((Member e) => e.user.id == channel.client.state.user.id)) {
return SizedBox();
}
return ChannelUnreadIndicator(
channel: channel,
);
}),
],
),
subtitle: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Flexible(child: _buildSubtitle(context)),
Builder(
builder: (context) {
final lastMessage = channel.state.messages.lastWhere(
(m) => !m.isDeleted && m.shadowed != true,
orElse: () => null,
);
if (lastMessage?.user?.id == StreamChat.of(context).user.id) {
return Padding(
padding: const EdgeInsets.only(right: 4.0),
child: SendingIndicator(
message: lastMessage,
size: StreamChatTheme.of(context).channelPreviewTheme.indicatorIconSize,
isMessageRead: channel.state.read
?.where((element) => element.user.id != channel.client.state.user.id)
?.where((element) => element.lastRead.isAfter(lastMessage.createdAt))
?.isNotEmpty ==
true,
),
);
}
return SizedBox();
},
),
_buildDate(context),
],
),
),
);
});
}
Widget _buildDate(BuildContext context) {
return StreamBuilder<DateTime>(
stream: channel.lastMessageAtStream,
initialData: channel.lastMessageAt,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return SizedBox();
}
final lastMessageAt = snapshot.data.toLocal();
String stringDate;
final now = DateTime.now();
var startOfDay = DateTime(now.year, now.month, now.day);
if (lastMessageAt.millisecondsSinceEpoch >= startOfDay.millisecondsSinceEpoch) {
stringDate = Jiffy(lastMessageAt.toLocal()).format('HH:mm');
} else if (lastMessageAt.millisecondsSinceEpoch >=
startOfDay.subtract(Duration(days: 1)).millisecondsSinceEpoch) {
stringDate = 'Yesterday';
} else if (startOfDay.difference(lastMessageAt).inDays < 7) {
stringDate = Jiffy(lastMessageAt.toLocal()).EEEE;
} else {
stringDate = Jiffy(lastMessageAt.toLocal()).format('dd/MM/yyyy');
}
return Text(
stringDate,
style: StreamChatTheme.of(context).channelPreviewTheme.lastMessageAt,
);
},
);
}
Widget _buildSubtitle(BuildContext context) {
if (channel.isMuted) {
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
StreamSvgIcon.mute(
size: 16,
),
Text(
' Channel is muted',
style: StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
color: StreamChatTheme.of(context).channelPreviewTheme.subtitle.color,
),
),
],
);
}
return TypingIndicator(
channel: channel,
alternativeWidget: _buildLastMessage(context),
style: StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
color: StreamChatTheme.of(context).channelPreviewTheme.subtitle.color,
),
);
}
Widget _buildLastMessage(BuildContext context) {
return StreamBuilder<List<Message>>(
stream: channel.state.messagesStream,
initialData: channel.state.messages,
builder: (context, snapshot) {
final lastMessage = snapshot.data?.lastWhere((m) => m.shadowed != true && !m.isDeleted, orElse: () => null);
if (lastMessage == null) {
return SizedBox();
}
var text = lastMessage.text;
if (lastMessage.attachments != null) {
final parts = <String>[
...lastMessage.attachments.map((e) {
if (e.type == 'image') {
return '📷';
} else if (e.type == 'video') {
return '🎬';
} else if (e.type == 'giphy') {
return '[GIF]';
}
return e == lastMessage.attachments.last ? (e.title ?? 'File') : '${e.title ?? 'File'} , ';
}).where((e) => e != null),
lastMessage.text ?? '',
];
text = parts.join(' ');
}
return Text.rich(
_getDisplayText(
text,
lastMessage.mentionedUsers,
lastMessage.attachments,
StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
color: StreamChatTheme.of(context).channelPreviewTheme.subtitle.color,
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) ? FontStyle.italic : FontStyle.normal),
StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
color: StreamChatTheme.of(context).channelPreviewTheme.subtitle.color,
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) ? FontStyle.italic : FontStyle.normal,
fontWeight: FontWeight.bold),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
},
);
}
TextSpan _getDisplayText(String text, List<User> mentions, List<Attachment> attachments, TextStyle normalTextStyle,
TextStyle mentionsTextStyle) {
var textList = text.split(' ');
var resList = <TextSpan>[];
for (var e in textList) {
if (mentions != null && mentions.isNotEmpty && mentions.any((element) => '@${element.name}' == e)) {
resList.add(TextSpan(
text: '$e ',
style: mentionsTextStyle,
));
} else if (attachments != null &&
attachments.isNotEmpty &&
attachments.where((e) => e.title != null).any((element) => element.title == e)) {
resList.add(TextSpan(
text: '$e ',
style: normalTextStyle.copyWith(fontStyle: FontStyle.italic),
));
} else {
resList.add(TextSpan(
text: e == textList.last ? '$e' : '$e ',
style: normalTextStyle,
));
}
}
return TextSpan(children: resList);
}
}
class ChannelUnreadIndicator extends StatelessWidget {
const ChannelUnreadIndicator({
Key key,
@required this.channel,
}) : super(key: key);
final Channel channel;
@override
Widget build(BuildContext context) {
return StreamBuilder<int>(
stream: channel.state.unreadCountStream,
initialData: channel.state.unreadCount,
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == 0) {
return SizedBox();
}
return Material(
borderRadius: BorderRadius.circular(8),
color: StreamChatTheme.of(context).channelPreviewTheme.unreadCounterColor,
child: Padding(
padding: const EdgeInsets.only(
left: 5.0,
right: 5.0,
top: 2,
bottom: 1,
),
child: Center(
child: Text(
'${snapshot.data > 99 ? '99+' : snapshot.data}',
style: TextStyle(
fontSize: 11,
color: Colors.white,
),
),
),
),
);
},
);
}
}
@@ -0,0 +1,160 @@
import 'package:stream_chatter/ui/common/my_channel_preview.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class ChatView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final textColor = Theme.of(context).appBarTheme.color;
return Scaffold(
backgroundColor: Theme.of(context).canvasColor,
appBar: AppBar(
title: Text(
'Chats',
style: TextStyle(
fontSize: 24,
color: textColor,
fontWeight: FontWeight.w800,
),
),
centerTitle: false,
elevation: 0,
backgroundColor: Theme.of(context).canvasColor,
),
body: ChannelsBloc(
child: ChannelListView(
filter: {
'members': {
'\$in': [StreamChat.of(context).user?.id],
}
},
sort: [SortOption('last_message_at')],
channelPreviewBuilder: (context, channel) {
return Container(
color: Theme.of(context).canvasColor,
child: MyChannelPreview(
channel: channel,
heroTag: channel.id,
onImageTap: () {
String name;
String image;
final currentUser = StreamChat.of(context).client.state.user;
if (channel.isGroup) {
name = channel.extraData['name'];
image = channel.extraData['image'];
} else {
final friend =
channel.state.members.where((element) => element.userId != currentUser.id).first.user;
name = friend.name;
image = friend.extraData['image'];
}
return Navigator.of(context).push(
PageRouteBuilder(
barrierColor: Colors.black45,
barrierDismissible: true,
opaque: false,
pageBuilder: (context, animation1, _) {
return FadeTransition(
opacity: animation1,
child: ChatDetailView(
channelId: channel.id,
image: image,
name: name,
),
);
}),
);
},
onTap: (channel) => {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return StreamChannel(
child: ChannelPage(),
channel: channel,
);
},
),
)
},
),
);
},
channelWidget: ChannelPage(),
),
),
);
}
}
class ChannelPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: ChannelHeader(),
body: Column(
children: [
Expanded(
child: MessageListView(),
),
MessageInput(),
],
),
);
}
}
class ChatDetailView extends StatelessWidget {
const ChatDetailView({
Key key,
this.image,
this.name,
this.channelId,
}) : super(key: key);
final String image;
final String name;
final String channelId;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: Navigator.of(context).pop,
child: Material(
color: Colors.transparent,
child: Dialog(
backgroundColor: Colors.transparent,
elevation: 0,
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Hero(
tag: channelId,
child: ClipOval(
child: Image.network(
image,
height: 180,
width: 180,
fit: BoxFit.cover,
),
),
),
Text(
name,
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 22,
),
),
],
),
),
),
),
);
}
}
@@ -0,0 +1,38 @@
import 'package:stream_chatter/data/stream_api_repository.dart';
import 'package:stream_chatter/domain/models/chat_user.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class ChatUserState {
const ChatUserState(this.chatUser, {this.selected = false});
final ChatUser chatUser;
final bool selected;
}
class FriendsSelectionCubit extends Cubit<List<ChatUserState>> {
FriendsSelectionCubit(this._streamApiRepository) : super([]);
final StreamApiRepository _streamApiRepository;
List<ChatUserState> get selectedUsers => state.where((element) => element.selected).toList();
Future<void> init() async {
final chatUsers = (await _streamApiRepository.getChatUsers()).map((e) => ChatUserState(e)).toList();
emit(chatUsers);
}
void selectUser(ChatUserState chatUser) {
final index = state.indexWhere((element) => element.chatUser.id == chatUser.chatUser.id);
state[index] = ChatUserState(state[index].chatUser, selected: !chatUser.selected);
emit(List<ChatUserState>.from(state));
}
Future<Channel> createFriendChannel(ChatUserState chatUserState) async {
return await _streamApiRepository.createSimpleChat(chatUserState.chatUser.id);
}
}
class FriendsGroupCubit extends Cubit<bool> {
FriendsGroupCubit() : super(false);
void changeToGroup() => emit(!state);
}
@@ -0,0 +1,190 @@
import 'package:stream_chatter/navigator_utils.dart';
import 'package:stream_chatter/ui/home/chat/chat_view.dart';
import 'package:stream_chatter/ui/home/chat/selection/friends_selection_cubit.dart';
import 'package:stream_chatter/ui/home/chat/selection/group_selection_view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class FriendsSelectionView extends StatelessWidget {
void _createFriendChannel(BuildContext context, ChatUserState chatUserState) async {
final channel = await context.read<FriendsSelectionCubit>().createFriendChannel(chatUserState);
pushAndReplaceToPage(
context,
Scaffold(
body: StreamChannel(
channel: channel,
child: ChannelPage(),
),
),
);
}
@override
Widget build(BuildContext context) {
final textColor = Theme.of(context).appBarTheme.color;
final accentColor = Theme.of(context).accentColor;
return MultiBlocProvider(
providers: [
BlocProvider(create: (context) => FriendsSelectionCubit(context.read())..init()),
BlocProvider(create: (_) => FriendsGroupCubit()),
],
child: BlocBuilder<FriendsGroupCubit, bool>(builder: (context, isGroup) {
return BlocBuilder<FriendsSelectionCubit, List<ChatUserState>>(builder: (context, snapshot) {
final selectedUsers = context.read<FriendsSelectionCubit>().selectedUsers;
return Scaffold(
floatingActionButton: isGroup && selectedUsers.isNotEmpty
? FloatingActionButton(
child: Icon(Icons.arrow_right_alt_rounded),
onPressed: () {
pushAndReplaceToPage(context, GroupSelectionView(selectedUsers));
})
: null,
backgroundColor: Theme.of(context).canvasColor,
body: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10.0,
vertical: 20,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (isGroup)
Row(
children: [
BackButton(
onPressed: () {
context.read<FriendsGroupCubit>().changeToGroup();
},
),
Text(
'New Group',
style: TextStyle(
fontSize: 24,
color: textColor,
fontWeight: FontWeight.w800,
),
),
],
)
else
Row(
children: [
BackButton(
onPressed: Navigator.of(context).pop,
),
Text(
'People',
style: TextStyle(
fontSize: 24,
color: textColor,
fontWeight: FontWeight.w800,
),
),
],
),
if (!isGroup)
ListTile(
onTap: context.read<FriendsGroupCubit>().changeToGroup,
leading: CircleAvatar(
backgroundColor: accentColor,
child: Icon(Icons.group_outlined),
),
title: Text('Create group', style: TextStyle(fontWeight: FontWeight.w700)),
subtitle: Text('Talk with 2 or more contacts'),
)
else if (isGroup && selectedUsers.isEmpty)
Padding(
padding: const EdgeInsets.only(top: 15.0, left: 20.0, bottom: 20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
backgroundColor: Colors.grey[200],
),
Text(
'Add a friend',
style: TextStyle(
fontSize: 12,
color: Colors.grey[500],
),
),
],
),
)
else
SizedBox(
height: 100,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: selectedUsers.length,
itemBuilder: (context, index) {
final chatUserState = selectedUsers[index];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 13.0),
child: Stack(
clipBehavior: Clip.none,
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
CircleAvatar(
radius: 30,
backgroundImage: NetworkImage(chatUserState.chatUser.image),
),
Text(chatUserState.chatUser.name),
],
),
Positioned(
bottom: 40,
right: -4,
child: InkWell(
onTap: () => context.read<FriendsSelectionCubit>().selectUser(chatUserState),
child: CircleAvatar(
radius: 9,
backgroundColor: accentColor,
child: Icon(Icons.close_rounded, size: 12),
),
),
),
],
),
);
})),
Expanded(
child: ListView.builder(
itemCount: snapshot.length,
itemBuilder: (context, index) {
final chatUserState = snapshot[index];
return ListTile(
onTap: () {
_createFriendChannel(context, chatUserState);
},
leading: CircleAvatar(
backgroundImage: NetworkImage(chatUserState.chatUser.image),
),
title: Text(chatUserState.chatUser.name),
trailing: isGroup
? Checkbox(
value: chatUserState.selected,
onChanged: (val) {
print('select user for group');
context.read<FriendsSelectionCubit>().selectUser(chatUserState);
},
)
: null,
);
},
),
),
],
),
),
);
});
}),
);
}
}
@@ -0,0 +1,47 @@
import 'dart:io';
import 'package:stream_chatter/data/image_picker_repository.dart';
import 'package:stream_chatter/domain/usecases/create_group_usecase.dart';
import 'package:stream_chatter/ui/home/chat/selection/friends_selection_cubit.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class GroupSelectionState {
const GroupSelectionState(
this.file, {
this.channel,
this.isLoading = false,
});
final File file;
final Channel channel;
final bool isLoading;
}
class GroupSelectionCubit extends Cubit<GroupSelectionState> {
GroupSelectionCubit(
this.members,
this._createGroupUseCase,
this._imagePickerRepository,
) : super(GroupSelectionState(null));
final nameTextController = TextEditingController();
final List<ChatUserState> members;
final CreateGroupUseCase _createGroupUseCase;
final ImagePickerRepository _imagePickerRepository;
void createGroup() async {
emit(GroupSelectionState(state.file, isLoading: true));
final channel = await _createGroupUseCase.createGroup(CreateGroupInput(
imageFile: state.file,
members: members.map((e) => e.chatUser.id).toList(),
name: nameTextController.text,
));
emit(GroupSelectionState(state.file, channel: channel, isLoading: false));
}
void pickImage() async {
final image = await _imagePickerRepository.pickImage();
emit(GroupSelectionState(image));
}
}
@@ -0,0 +1,117 @@
import 'package:stream_chatter/navigator_utils.dart';
import 'package:stream_chatter/ui/common/avatar_image_view.dart';
import 'package:stream_chatter/ui/common/loading_view.dart';
import 'package:stream_chatter/ui/home/chat/chat_view.dart';
import 'package:stream_chatter/ui/home/chat/selection/friends_selection_cubit.dart';
import 'package:stream_chatter/ui/home/chat/selection/group_selection_cubit.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class GroupSelectionView extends StatelessWidget {
GroupSelectionView(this.selectedUsers);
final List<ChatUserState> selectedUsers;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => GroupSelectionCubit(
selectedUsers,
context.read(),
context.read(),
),
child: BlocConsumer<GroupSelectionCubit, GroupSelectionState>(listener: (context, snapshot) {
if (snapshot.channel != null) {
pushAndReplaceToPage(
context,
Scaffold(
body: StreamChannel(
channel: snapshot.channel,
child: ChannelPage(),
),
),
);
}
}, builder: (context, snapshot) {
return LoadingView(
isLoading: snapshot.isLoading,
child: Scaffold(
floatingActionButton: FloatingActionButton(
child: Icon(Icons.arrow_right_alt_rounded),
onPressed: context.read<GroupSelectionCubit>().createGroup,
),
backgroundColor: Theme.of(context).canvasColor,
appBar: AppBar(
title: Text(
'New Group',
style: TextStyle(
fontSize: 24,
color: Theme.of(context).appBarTheme.color,
fontWeight: FontWeight.w800,
),
),
centerTitle: false,
elevation: 0,
backgroundColor: Theme.of(context).canvasColor,
),
body: Column(
children: [
AvatarImageView(
onTap: context.read<GroupSelectionCubit>().pickImage,
child: snapshot?.file != null
? Image.file(
snapshot?.file,
fit: BoxFit.cover,
)
: Icon(
Icons.person_outline,
size: 100,
color: Colors.grey[400],
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 50.0,
vertical: 20,
),
child: TextField(
controller: context.read<GroupSelectionCubit>().nameTextController,
decoration: InputDecoration(
fillColor: Theme.of(context).bottomNavigationBarTheme.backgroundColor,
hintText: 'Name of the group',
hintStyle: TextStyle(
fontSize: 13,
color: Colors.grey[400],
),
border: InputBorder.none,
enabledBorder: InputBorder.none,
),
),
),
Wrap(
children: List.generate(selectedUsers.length, (index) {
final chatUserState = selectedUsers[index];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircleAvatar(
radius: 30,
backgroundImage: NetworkImage(chatUserState.chatUser.image),
),
Text(chatUserState.chatUser.name),
],
),
);
}),
),
],
),
),
);
}),
);
}
}
@@ -0,0 +1,7 @@
import 'package:flutter_bloc/flutter_bloc.dart';
class HomeCubit extends Cubit<int> {
HomeCubit() : super(0);
void onChangeTab(int index) => emit(index);
}
+139
View File
@@ -0,0 +1,139 @@
import 'package:stream_chatter/navigator_utils.dart';
import 'package:stream_chatter/ui/home/chat/chat_view.dart';
import 'package:stream_chatter/ui/home/chat/selection/friends_selection_view.dart';
import 'package:stream_chatter/ui/home/home_cubit.dart';
import 'package:stream_chatter/ui/home/settings/settings_view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class HomeView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).canvasColor,
body: BlocProvider(
create: (_) => HomeCubit(),
child: Column(
children: [
Expanded(
child: BlocBuilder<HomeCubit, int>(builder: (context, snapshot) {
return IndexedStack(
index: snapshot,
children: [
ChatView(),
SettingsView(),
],
);
}),
),
HomeNavigationBar(),
],
),
),
);
}
}
class HomeNavigationBar extends StatelessWidget {
const HomeNavigationBar({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final cubit = BlocProvider.of<HomeCubit>(context, listen: true);
final navigationBarSize = 80.0;
final buttonSize = 56.0;
final buttonMargin = 4.0;
final topMargin = buttonSize / 2 + buttonMargin / 2;
final canvasColor = Theme.of(context).canvasColor;
return Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: Material(
child: Container(
height: navigationBarSize + topMargin,
width: MediaQuery.of(context).size.width * 0.7,
color: canvasColor,
child: Stack(
children: [
Positioned.fill(
top: topMargin,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25),
color: Theme.of(context).bottomNavigationBarTheme.backgroundColor,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_HomeNavItem(
text: 'Chats',
iconData: Icons.chat_bubble,
onTap: () => cubit.onChangeTab(0),
selected: cubit.state == 0,
),
_HomeNavItem(
text: 'Settings',
iconData: Icons.settings,
onTap: () => cubit.onChangeTab(1),
selected: cubit.state == 1,
),
],
),
),
),
Align(
alignment: Alignment.topCenter,
child: Container(
decoration: BoxDecoration(
color: canvasColor,
shape: BoxShape.circle,
),
padding: EdgeInsets.all(buttonMargin / 2),
child: FloatingActionButton(
onPressed: () {
pushToPage(context, FriendsSelectionView());
},
child: Icon(Icons.add),
),
),
),
],
),
),
),
);
}
}
class _HomeNavItem extends StatelessWidget {
const _HomeNavItem({
Key key,
this.iconData,
this.text,
this.onTap,
this.selected = false,
}) : super(key: key);
final IconData iconData;
final String text;
final VoidCallback onTap;
final bool selected;
@override
Widget build(BuildContext context) {
final selectedColor = Theme.of(context).bottomNavigationBarTheme.selectedItemColor;
final unselectedColor = Theme.of(context).bottomNavigationBarTheme.unselectedItemColor;
final color = selected ? selectedColor : unselectedColor;
return GestureDetector(
onTap: onTap,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(iconData, color: color),
Text(text, style: TextStyle(color: color)),
],
),
);
}
}
@@ -0,0 +1,19 @@
import 'package:stream_chatter/domain/usecases/logout_usecase.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SettingsSwitchCubit extends Cubit<bool> {
SettingsSwitchCubit(bool state) : super(state);
void onChangeDarkMode(bool isDark) => emit(isDark);
}
class SettingsLogoutCubit extends Cubit<void> {
SettingsLogoutCubit(this._logoutUseCase) : super(null);
final LogoutUseCase _logoutUseCase;
void logOut() async {
await _logoutUseCase.logout();
emit(null);
}
}
@@ -0,0 +1,119 @@
import 'package:stream_chatter/navigator_utils.dart';
import 'package:stream_chatter/ui/app_theme_cubit.dart';
import 'package:stream_chatter/ui/common/avatar_image_view.dart';
import 'package:stream_chatter/ui/home/settings/settings_cubit.dart';
import 'package:stream_chatter/ui/sign_in/sign_in_view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class SettingsView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final user = StreamChat.of(context).client.state.user;
final image = user?.extraData['image'];
final textColor = Theme.of(context).appBarTheme.color;
return MultiBlocProvider(
providers: [
BlocProvider(
create: (_) => SettingsSwitchCubit(context.read<AppThemeCubit>().isDark),
),
BlocProvider(
create: (_) => SettingsLogoutCubit(context.read()),
),
],
child: Scaffold(
backgroundColor: Theme.of(context).canvasColor,
appBar: AppBar(
title: Text(
'Settings',
style: TextStyle(
fontSize: 24,
color: textColor,
fontWeight: FontWeight.w800,
),
),
centerTitle: false,
elevation: 0,
backgroundColor: Theme.of(context).canvasColor,
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(15.0),
child: Column(
children: [
AvatarImageView(
//TODO: implement change avatar
onTap: () => null,
child: image != null
? Image.network(
image,
fit: BoxFit.cover,
)
: Icon(
Icons.person_outline,
size: 100,
color: Colors.grey[400],
),
),
Text(
user.name,
style: TextStyle(
fontSize: 24,
color: textColor,
fontWeight: FontWeight.w800,
),
),
Row(
children: [
Icon(Icons.nights_stay_outlined),
const SizedBox(width: 10),
Text(
'Dark Mode',
style: TextStyle(
color: textColor,
),
),
Spacer(),
BlocBuilder<SettingsSwitchCubit, bool>(builder: (context, snapshot) {
return Switch(
value: snapshot,
onChanged: (val) {
context.read<SettingsSwitchCubit>().onChangeDarkMode(val);
context.read<AppThemeCubit>().updateTheme(val);
},
);
}),
],
),
const SizedBox(height: 15),
Builder(builder: (context) {
return GestureDetector(
onTap: context.read<SettingsLogoutCubit>().logOut,
child: BlocListener<SettingsLogoutCubit, void>(
listener: (context, snapshot) {
popAllAndPush(context, SignInView());
},
child: Row(children: [
Icon(Icons.logout),
const SizedBox(width: 10),
Text(
'Logout',
style: TextStyle(
color: textColor,
),
),
Spacer(),
Icon(Icons.arrow_right),
]),
),
);
}),
],
),
),
),
),
);
}
}
@@ -0,0 +1,44 @@
import 'dart:io';
import 'package:stream_chatter/data/image_picker_repository.dart';
import 'package:stream_chatter/domain/usecases/profile_sign_in_usecase.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ProfileState {
const ProfileState(
this.file, {
this.success = false,
this.loading = false,
});
final File file;
final bool success;
final bool loading;
}
class ProfileVerifyCubit extends Cubit<ProfileState> {
ProfileVerifyCubit(
this._imagePickerRepository,
this._profileSignInUseCase,
) : super(ProfileState(null));
final nameController = TextEditingController();
final ImagePickerRepository _imagePickerRepository;
final ProfileSignInUseCase _profileSignInUseCase;
void startChatting() async {
emit(ProfileState(null, loading: true));
final file = state.file;
final name = nameController.text;
await _profileSignInUseCase.verify(ProfileInput(
imageFile: file,
name: name,
));
emit(ProfileState(file, success: true, loading: false));
}
void pickImage() async {
final file = await _imagePickerRepository.pickImage();
emit(ProfileState(file));
}
}
@@ -0,0 +1,108 @@
import 'package:stream_chatter/navigator_utils.dart';
import 'package:stream_chatter/ui/common/avatar_image_view.dart';
import 'package:stream_chatter/ui/common/loading_view.dart';
import 'package:stream_chatter/ui/home/home_view.dart';
import 'package:stream_chatter/ui/profile_verify/profile_verify_cubit.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class ProfileVerifyView extends StatelessWidget {
ProfileVerifyView();
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => ProfileVerifyCubit(context.read(), context.read()),
child: BlocConsumer<ProfileVerifyCubit, ProfileState>(listener: (context, snapshot) {
if (snapshot.success) {
pushAndReplaceToPage(context, HomeView());
}
}, builder: (context, snapshot) {
//refresh the photo
return LoadingView(
isLoading: snapshot.loading,
child: Scaffold(
backgroundColor: Theme.of(context).canvasColor,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Verify your identity',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w800,
),
),
AvatarImageView(
onTap: context.read<ProfileVerifyCubit>().pickImage,
child: snapshot.file != null
? Image.file(
snapshot.file,
fit: BoxFit.cover,
)
: Icon(
Icons.person_outline,
size: 100,
color: Colors.grey[400],
),
),
Text(
'Your name',
style: TextStyle(
fontWeight: FontWeight.w700,
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 50.0,
vertical: 20,
),
child: TextField(
controller: context.read<ProfileVerifyCubit>().nameController,
decoration: InputDecoration(
fillColor: Theme.of(context).bottomNavigationBarTheme.backgroundColor,
hintText: 'Or just how people now you',
hintStyle: TextStyle(
fontSize: 13,
color: Colors.grey[400],
),
border: InputBorder.none,
enabledBorder: InputBorder.none,
),
),
),
Hero(
tag: 'home_hero',
child: Material(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
color: Theme.of(context).accentColor,
child: InkWell(
onTap: () {
context.read<ProfileVerifyCubit>().startChatting();
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 40.0,
vertical: 15,
),
child: Text(
'Start chatting now',
style: TextStyle(
color: Colors.white,
),
),
)),
),
),
],
),
),
),
);
}),
);
}
}
@@ -0,0 +1,29 @@
import 'package:stream_chatter/domain/usecases/login_usecase.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
enum SignInState {
none,
existing_user,
}
class SignInCubit extends Cubit<SignInState> {
SignInCubit(
this._loginUseCase,
) : super(SignInState.none);
final LoginUseCase _loginUseCase;
void signIn() async {
try {
final result = await _loginUseCase.validateLogin();
if (result) {
emit(SignInState.existing_user);
}
} catch (ex) {
final result = await _loginUseCase.signIn();
if (result != null) {
emit(SignInState.none);
}
}
}
}
@@ -0,0 +1,100 @@
import 'package:stream_chatter/navigator_utils.dart';
import 'package:stream_chatter/ui/home/home_view.dart';
import 'package:stream_chatter/ui/profile_verify/profile_verify_view.dart';
import 'package:stream_chatter/ui/sign_in/sign_in_cubit.dart';
import 'package:stream_chatter/ui/common/initial_background_view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SignInView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => SignInCubit(context.read()),
child: BlocConsumer<SignInCubit, SignInState>(listener: (context, snapshot) {
if (snapshot == SignInState.none) {
pushAndReplaceToPage(context, ProfileVerifyView());
} else {
pushAndReplaceToPage(context, HomeView());
}
}, builder: (context, snapshot) {
return Scaffold(
backgroundColor: Theme.of(context).canvasColor,
body: Stack(children: [
InitialBackgroundView(),
Padding(
padding: const EdgeInsets.only(left: 25.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 150),
Hero(
tag: 'logo_hero',
child: Image.asset(
'assets/logo.png',
height: 50,
),
),
const SizedBox(height: 30),
Text(
'Welcome to\nChatty',
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.w900,
),
),
Padding(
padding: const EdgeInsets.only(top: 8.0, bottom: 40),
child: Text(
'A platform to chat with users very easily and friendly',
style: TextStyle(
fontWeight: FontWeight.w600,
color: Colors.grey,
),
),
),
Material(
elevation: 2,
shadowColor: Colors.black45,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
color: Theme.of(context).bottomNavigationBarTheme.backgroundColor,
child: InkWell(
onTap: () {
context.read<SignInCubit>().signIn();
},
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset('assets/icon-google.png', height: 20),
const SizedBox(width: 15),
Text('Login with Google'),
],
),
),
),
),
Spacer(),
Align(
alignment: Alignment.bottomLeft,
child: Text(
'"in the modern world the\nquality of life is the quality\nof communication',
style: TextStyle(
fontWeight: FontWeight.w400,
color: Colors.grey,
),
),
),
const SizedBox(height: 20),
],
),
),
]),
);
}),
);
}
}
@@ -0,0 +1,32 @@
import 'package:stream_chatter/domain/exceptions/auth_exception.dart';
import 'package:stream_chatter/domain/usecases/login_usecase.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
enum SplashState {
none,
existing_user,
new_user,
}
class SplashCubit extends Cubit<SplashState> {
SplashCubit(
this._loginUseCase,
) : super(SplashState.none);
final LoginUseCase _loginUseCase;
void init() async {
try {
final result = await _loginUseCase.validateLogin();
if (result) {
emit(SplashState.existing_user);
}
} on AuthException catch (ex) {
if (ex.error == AuthErrorCode.not_auth) {
emit(SplashState.none);
} else {
emit(SplashState.new_user);
}
}
}
}
@@ -0,0 +1,44 @@
import 'package:stream_chatter/navigator_utils.dart';
import 'package:stream_chatter/ui/home/home_view.dart';
import 'package:stream_chatter/ui/profile_verify/profile_verify_view.dart';
import 'package:stream_chatter/ui/sign_in/sign_in_view.dart';
import 'package:stream_chatter/ui/common/initial_background_view.dart';
import 'package:stream_chatter/ui/splash/splash_cubit.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SplashView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => SplashCubit(context.read())..init(),
child: BlocListener<SplashCubit, SplashState>(
listener: (context, snapshot) {
if (snapshot == SplashState.none) {
pushAndReplaceToPage(context, SignInView());
} else if (snapshot == SplashState.existing_user) {
pushAndReplaceToPage(context, HomeView());
} else {
pushAndReplaceToPage(context, ProfileVerifyView());
}
},
child: Scaffold(
body: Stack(
children: [
InitialBackgroundView(),
Center(
child: Hero(
tag: 'logo_hero',
child: Image.asset(
'assets/logo.png',
height: 100,
),
),
),
],
),
),
),
);
}
}
+54
View File
@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
const primaryColor = Color(0xFF3883FB);
const backgroundLightColor = Color(0xFFFCFCFC);
const backgroundDarkColor = Color(0xFF1F2026);
const navigationBarLightColor = Colors.white;
const navigationBarDarkColor = Color(0xFF30313C);
class Themes {
static final themeLight = ThemeData.light().copyWith(
backgroundColor: backgroundLightColor,
// selected color
accentColor: primaryColor,
// floating action button
floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: primaryColor,
foregroundColor: Colors.white,
),
// bottom bar
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: navigationBarLightColor,
selectedItemColor: primaryColor,
unselectedItemColor: Colors.grey[200],
),
// switch active color
toggleableActiveColor: primaryColor,
canvasColor: backgroundLightColor,
appBarTheme: AppBarTheme(
color: Colors.black,
));
static final themeDark = ThemeData.dark().copyWith(
backgroundColor: backgroundDarkColor,
// selected color
accentColor: primaryColor,
// floating action button
floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: primaryColor,
foregroundColor: Colors.white,
),
// bottom bar
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: navigationBarDarkColor,
selectedItemColor: primaryColor,
unselectedItemColor: Colors.grey[300],
),
textSelectionColor: Colors.white,
// switch active color
toggleableActiveColor: primaryColor,
canvasColor: backgroundDarkColor,
appBarTheme: AppBarTheme(
color: Colors.white,
));
}