├── android ├── settings_aar.gradle ├── gradle.properties ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── ic_launcher_monochrome.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── ic_launcher_monochrome.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── ic_launcher_monochrome.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── ic_launcher_monochrome.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── ic_launcher_monochrome.png │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ │ └── ic_launcher.xml │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ └── values │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── one9x │ │ │ │ │ └── vartalap │ │ │ │ │ ├── MainActivity.kt │ │ │ │ │ └── Application.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle └── build.gradle ├── ios ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme └── .gitignore ├── web ├── favicon.png ├── icons │ ├── Icon-192.png │ └── Icon-512.png ├── manifest.json └── index.html ├── fonts └── Sofia Pro Medium Az.otf ├── lib ├── utils │ ├── find.dart │ ├── url_helper.dart │ ├── enum_helper.dart │ ├── phone_number.dart │ ├── remote_message_helper.dart │ ├── dateTimeFormat.dart │ ├── color_helper.dart │ └── chat_message_helper.dart ├── widgets │ ├── notifier │ │ └── iterable_notifier.dart │ ├── profile_img.dart │ ├── Inherited │ │ ├── config_provider.dart │ │ ├── current_user.dart │ │ └── auth_listener.dart │ ├── app_logo.dart │ ├── contactPreviewItem.dart │ ├── avator.dart │ ├── loadingIndicator.dart │ ├── contact.dart │ ├── avator_letter.dart │ ├── keyboard.dart │ ├── rich_message.dart │ ├── chat_preview.dart │ ├── message.dart │ ├── message_input.dart │ └── chatlist.dart ├── models │ ├── dateHeader.dart │ ├── previewImage.dart │ ├── messageSpacer.dart │ ├── user.dart │ ├── chat.dart │ └── remoteMessage.dart ├── services │ ├── crashlystics.dart │ ├── performance_metric.dart │ ├── auth_service.dart │ ├── push_notification_service.dart │ ├── api_service.dart │ └── user_service.dart ├── config │ └── config_store.dart ├── dataAccessLayer │ └── db.dart ├── screens │ ├── startup │ │ └── startup.dart │ ├── login │ │ ├── introduction.dart │ │ └── verifyOtp.dart │ └── new_chat │ │ ├── create_group.dart │ │ └── select_group_member.dart ├── theme │ └── theme.dart └── main.dart ├── .metadata ├── .theia └── launch.json ├── .gitpod.yml ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── PULL_REQUEST_TEMPLATE │ └── pull_request_template.md ├── .vscode └── launch.json ├── config.json.tmpl ├── .gitignore ├── .gitpod.Dockerfile ├── SECURITY.md ├── test └── widget_test.dart ├── CONTRIBUTING.md ├── README.md ├── pubspec.yaml └── CODE_OF_CONDUCT.md /android/settings_aar.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/web/icons/Icon-512.png -------------------------------------------------------------------------------- /fonts/Sofia Pro Medium Az.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/fonts/Sofia Pro Medium Az.otf -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /lib/utils/find.dart: -------------------------------------------------------------------------------- 1 | T? find(Iterable items, bool Function(T) fn) { 2 | try { 3 | return items.firstWhere(fn); 4 | } catch (_) { 5 | return null; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ramank775/vartalap/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/one9x/vartalap/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.one9x.vartalap 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/widgets/notifier/iterable_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | class SetNotifier extends ValueNotifier> { 4 | SetNotifier(Set value) : super(value); 5 | update() { 6 | this.notifyListeners(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/models/dateHeader.dart: -------------------------------------------------------------------------------- 1 | class DateHeader { 2 | final String date; 3 | 4 | const DateHeader({ 5 | required this.date, 6 | }); 7 | 8 | int get hashCode => this.date.hashCode; 9 | 10 | @override 11 | bool operator ==(Object other) { 12 | return hashCode == other.hashCode; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/models/previewImage.dart: -------------------------------------------------------------------------------- 1 | class PreviewImage { 2 | final String id; 3 | final String uri; 4 | const PreviewImage({required this.id, required this.uri}); 5 | 6 | int get hashCode => this.id.hashCode; 7 | 8 | @override 9 | bool operator ==(Object other) { 10 | return hashCode == other.hashCode; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 8fe7655ed20ffd1395f68e30539a847a01a30351 8 | channel: beta 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /lib/utils/url_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:url_launcher/url_launcher.dart' as launcher; 2 | 3 | launchUrl(String link) async { 4 | var uri = Uri.tryParse(link); 5 | if (uri == null) return; 6 | if (!uri.hasScheme) { 7 | uri = Uri.http(link, ''); 8 | } 9 | if (await launcher.canLaunchUrl(uri)) await launcher.launchUrl(uri); 10 | } 11 | -------------------------------------------------------------------------------- /lib/models/messageSpacer.dart: -------------------------------------------------------------------------------- 1 | class MessageSpacer { 2 | final double height; 3 | final String id; 4 | const MessageSpacer({ 5 | required this.height, 6 | required this.id, 7 | }); 8 | 9 | int get hashCode => this.id.hashCode; 10 | 11 | @override 12 | bool operator ==(Object other) { 13 | return hashCode == other.hashCode; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.theia/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Flutter", 6 | "request": "launch", 7 | "type": "dart", 8 | "args": [ 9 | "-d", 10 | "web-server", 11 | "--web-port", 12 | "8000" 13 | ], 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: 2 | file: .gitpod.Dockerfile 3 | 4 | tasks: 5 | - before: | 6 | export PATH=$ANDROID_HOME/bin:$ANDROID_HOME/platform-tools:$PATH 7 | init: | 8 | echo "Init Flutter..." 9 | cd /workspace/chat-flutter-app 10 | flutter upgrade 11 | flutter doctor --android-licenses 12 | flutter pub get 13 | command: | 14 | flutter pub upgrade 15 | echo "Ready to go!" 16 | flutter doctor 17 | vscode: 18 | extensions: 19 | - Dart-Code.dart-code@3.12.2:U4I/KVVS4Adq5Ain/7bqhg== 20 | - Dart-Code.flutter@3.12.2:8+9OCbCxNozE+NHjTY4Ubw== -------------------------------------------------------------------------------- /lib/widgets/profile_img.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | enum ProfileImgSize { MD, SM, OTHER } 4 | 5 | class ProfileImg extends StatelessWidget { 6 | final String _uri; 7 | final ProfileImgSize _size; 8 | ProfileImg(this._uri, this._size); 9 | 10 | @override 11 | Widget build(BuildContext context) { 12 | return new CircleAvatar( 13 | foregroundColor: Theme.of(context).primaryColor, 14 | backgroundColor: Colors.grey, 15 | backgroundImage: new AssetImage(this._uri), 16 | radius: this._size == ProfileImgSize.SM ? 15.0 : 20.0, 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/widgets/Inherited/config_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:vartalap/config/config_store.dart'; 3 | 4 | class ConfigProvider extends InheritedWidget { 5 | ConfigProvider({ 6 | Key? key, 7 | required this.configStore, 8 | required Widget child, 9 | }) : super(key: key, child: child); 10 | 11 | final ConfigStore configStore; 12 | 13 | static ConfigProvider of(BuildContext context) { 14 | return context.dependOnInheritedWidgetOfExactType()!; 15 | } 16 | 17 | @override 18 | bool updateShouldNotify(ConfigProvider oldWidget) => false; 19 | } 20 | -------------------------------------------------------------------------------- /lib/widgets/Inherited/current_user.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:vartalap/models/user.dart'; 3 | 4 | class CurrentUser extends InheritedWidget { 5 | CurrentUser({ 6 | Key? key, 7 | required this.user, 8 | required Widget child, 9 | }) : super(key: key, child: child); 10 | 11 | final User? user; 12 | 13 | static CurrentUser of(BuildContext context) { 14 | return context.dependOnInheritedWidgetOfExactType()!; 15 | } 16 | 17 | @override 18 | bool updateShouldNotify(CurrentUser oldWidget) => 19 | user?.username != oldWidget.user?.username; 20 | } 21 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/one9x/vartalap/Application.kt: -------------------------------------------------------------------------------- 1 | package com.one9x.vartalap 2 | 3 | import io.flutter.app.FlutterApplication 4 | import android.app.NotificationManager 5 | import android.content.Context 6 | 7 | class Application : FlutterApplication() { 8 | @Override 9 | override fun onCreate() { 10 | super.onCreate() 11 | cancelAllNotifications() 12 | } 13 | 14 | private fun cancelAllNotifications() { 15 | val notificationManager: NotificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 16 | notificationManager.cancelAll() 17 | } 18 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ramank775 # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # vartalap 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: [] 13 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Vartalap", 3 | "short_name": "Vartalap", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | include ':app' 6 | 7 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 8 | def properties = new Properties() 9 | 10 | assert localPropertiesFile.exists() 11 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 12 | 13 | def flutterSdkPath = properties.getProperty("flutter.sdk") 14 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 15 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEAT]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /lib/widgets/app_logo.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:vartalap/theme/theme.dart'; 3 | 4 | class AppLogo extends StatelessWidget { 5 | final double size; 6 | final Color backgroundColor; 7 | const AppLogo({ 8 | Key? key, 9 | required this.size, 10 | this.backgroundColor = Colors.white, 11 | }) : super(key: key); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | final theme = VartalapTheme.theme; 16 | return CircleAvatar( 17 | backgroundColor: this.backgroundColor, 18 | radius: this.size, 19 | child: Icon( 20 | Icons.chat_bubble_outline, 21 | color: theme.appLogoColor, 22 | size: this.size, 23 | ), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Vartalap-Dev", 9 | "program": "lib/main.dart", 10 | "request": "launch", 11 | "type": "dart", 12 | "args": [ 13 | "--flavor=dev" 14 | ] 15 | }, { 16 | "name": "Flutter:profile", 17 | "program": "lib/main.dart", 18 | "request": "launch", 19 | "type": "dart", 20 | "flutterMode": "profile" 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /lib/utils/enum_helper.dart: -------------------------------------------------------------------------------- 1 | const _OTHER_INDEX = -1; 2 | const _OTHER_STR = "other"; 3 | 4 | T intToEnum(int index, Iterable values) { 5 | if (index == _OTHER_INDEX || values.length - 1 < index) { 6 | return stringToEnum(_OTHER_STR, values); 7 | } 8 | return values.elementAt(index); 9 | } 10 | 11 | int enumToInt(T value, Iterable values) { 12 | if (value.toString().split('.')[1].toLowerCase() == _OTHER_STR) { 13 | return _OTHER_INDEX; 14 | } 15 | return values.toList().indexOf(value); 16 | } 17 | 18 | T stringToEnum(String value, Iterable values) { 19 | return values.firstWhere((element) => 20 | element.toString().split('.')[1].toLowerCase() == value.toLowerCase()); 21 | } 22 | 23 | String enumToString(T value) { 24 | return value.toString().toLowerCase().split('.')[1]; 25 | } 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Tap on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Smartphone (please complete the following information):** 27 | - Device: [e.g. Samsung Galaxy S20] 28 | - OS: [e.g. Android 10] 29 | - Version [e.g. 2.5.10] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /lib/utils/phone_number.dart: -------------------------------------------------------------------------------- 1 | // HACK: Currently as this app is intended to work for indian user's only 2 | // use +91 as default country code 3 | String? normalizePhoneNumber(String phoneNumber, {String countryCode = '+91'}) { 4 | // Remove space 5 | phoneNumber = phoneNumber.replaceAll(' ', ''); 6 | // Check if it's a valid phone number by parsing the string into integer 7 | if (int.tryParse(phoneNumber) == null) { 8 | return null; 9 | } 10 | 11 | // Check if number starts with 0, i.e. it's a local number replace 0 with country code 12 | // else if the number doesn't start's with + append the country code 13 | if (phoneNumber.startsWith('0')) { 14 | phoneNumber = countryCode + phoneNumber.substring(1); 15 | } else if (!phoneNumber.startsWith('+')) { 16 | phoneNumber = countryCode + phoneNumber; 17 | } 18 | return phoneNumber; 19 | } 20 | -------------------------------------------------------------------------------- /config.json.tmpl: -------------------------------------------------------------------------------- 1 | { 2 | "api_url": "", 3 | "ws_url": "", 4 | "description": "Vartalap is an open source personal chat messager. It is design to provide the level of transparency in the personal messaging application with your data.\n\nGithub Repo:\nFlutter App: - https://github.com/ramank775/vartalap\nBackend Server:- https://github.com/ramank775/chat-server \n\nIf you are an open source contributor and interested in contributing towards this app, reach out to me at twitter @vartalap_app\nIf you have found any issue feel free to raise a issue on the Github or email on the developer mail vartalap@one9x.org.\nPrivacy Policy: https://vartalap.one9x.org/privacy-policy.", 5 | "share_message": "Let's chat on vartalap. It's an open source personal chatting application. Get it at https://vartalap.one9x.org", 6 | "privacy_policy": "https://vartalap.one9x.org/privacy-policy" 7 | } -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.8.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.4.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | classpath 'com.google.gms:google-services:4.3.15' 12 | classpath 'com.google.firebase:perf-plugin:1.4.1' 13 | classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.8' 14 | 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | mavenCentral() 22 | } 23 | } 24 | 25 | rootProject.buildDir = '../build' 26 | subprojects { 27 | project.buildDir = "${rootProject.buildDir}/${project.name}" 28 | } 29 | subprojects { 30 | project.evaluationDependsOn(':app') 31 | } 32 | 33 | tasks.register("clean", Delete) { 34 | delete rootProject.buildDir 35 | } 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Exceptions to above rules. 44 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 45 | 46 | google-services.* 47 | config.json 48 | *.local.* 49 | GoogleService-Info.plist 50 | firebase_*.json -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Pull Request template 2 | **IMPORTANT: Please do not create a Pull Request without creating an issue first.** 3 | 4 | Please, go through these steps before you submit a PR. 5 | 6 | 1. Make sure that your PR is not a duplicate. 7 | 2. If not, then make sure that: 8 | 9 | a. You have done your changes in a separate branch. Branches MUST have descriptive names that start with either the `fix/` or `feature/` prefixes. Good examples are: `fix/signin-issue` or `feature/issue-templates`. 10 | 11 | b. You have a descriptive commit message with a short title (first line). 12 | 13 | c. You have only one commit (if not, squash them into one commit). 14 | 15 | 3. **After** these steps, you're ready to open a pull request. 16 | 17 | a. Give a descriptive title to your PR. 18 | 19 | b. Provide a description of your changes. 20 | 21 | c. Put `closes #XXXX` in your comment to auto-close the issue that your PR fixes (if such). 22 | 23 | 24 | **PLEASE REMOVE THIS TEMPLATE BEFORE SUBMITTING** -------------------------------------------------------------------------------- /.gitpod.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gitpod/workspace-flutter 2 | 3 | # Install custom tools, runtimes, etc. 4 | # For example "bastet", a command-line tetris clone: 5 | # RUN brew install bastet 6 | # 7 | # More information: https://www.gitpod.io/docs/config-docker/ 8 | ENV ANDROID_HOME=/home/gitpod/development/android-sdk 9 | ENV JAVA_HOME=/home/gitpod/.sdkman/candidates/java/current 10 | RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh \ 11 | && sdk install java 8.0.265.j9-adpt" 12 | 13 | RUN mkdir -p /home/gitpod/.android && \ 14 | touch /home/gitpod/.android/repositories.cfg 15 | 16 | RUN echo "Installing Android SDK..." && \ 17 | mkdir -p /home/gitpod/development/android-sdk && cd /home/gitpod/development/android-sdk && wget https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip && unzip sdk-tools-linux-4333796.zip && rm -f sdk-tools-linux-4333796.zip && \ 18 | chmod +x /home/gitpod/development/android-sdk/tools/bin/sdkmanager && \ 19 | yes | /home/gitpod/development/android-sdk/tools/bin/sdkmanager "platform-tools" "platforms;android-28" "build-tools;28.0.3" 20 | -------------------------------------------------------------------------------- /lib/widgets/contactPreviewItem.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:vartalap/models/user.dart'; 3 | 4 | import 'package:vartalap/widgets/avator.dart'; 5 | 6 | class ContactPreviewItem extends StatelessWidget { 7 | const ContactPreviewItem({ 8 | Key? key, 9 | required User user, 10 | }) : _user = user, 11 | super(key: key); 12 | 13 | final User _user; 14 | 15 | @override 16 | Widget build(BuildContext context) { 17 | return Container( 18 | child: Column( 19 | children: [ 20 | Avator( 21 | height: 45, 22 | width: 45, 23 | text: _user.name, 24 | ), 25 | SizedBox( 26 | height: 2, 27 | ), 28 | SizedBox( 29 | width: 60, 30 | child: Text( 31 | _user.name, 32 | maxLines: 2, 33 | textAlign: TextAlign.center, 34 | softWrap: true, 35 | overflow: TextOverflow.fade, 36 | ), 37 | ) 38 | ], 39 | ), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/widgets/avator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:vartalap/widgets/avator_letter.dart'; 3 | import 'package:vartalap/utils/color_helper.dart'; 4 | 5 | class Avator extends StatelessWidget { 6 | final String text; 7 | final double _opacity = 0.65; 8 | final double width; 9 | final double height; 10 | Avator({ 11 | Key? key, 12 | required this.text, 13 | required this.width, 14 | required this.height, 15 | }) : super(key: key); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | final brightness = Theme.of(context).brightness; 20 | return SizedBox( 21 | width: this.width, 22 | height: this.height, 23 | child: AvatarLetter( 24 | backgroundColor: getColor( 25 | this.text, 26 | opacity: this._opacity, 27 | brightness: brightness, 28 | ), 29 | text: this.text, 30 | numberLetters: 2, 31 | upperCase: true, 32 | letterType: LetterType.Circular, 33 | textColor: Colors.white, 34 | fontSize: 12, 35 | ), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | >2.5.4 | :white_check_mark: | 8 | | <2.5.4 | :x: | 9 | 10 | ## Reporting a Vulnerability 11 | 12 | We consider the security of our users and our platform a top priority. 13 | 14 | If you believe that you have discovered a potential vulnerability on our platform or in any APIs, apps or servers 15 | or any other assets please inform us via [security@one9x.org](mailto:security@one9x.org). 16 | 17 | We would like to know about it so we can take steps to address it as quickly as possible. 18 | 19 | Please look at our Security vulnerability Disclosure Policy for Do and Don't [here](https://one9x.org/security-disclosure-policy). 20 | 21 | ### What we promise: 22 | - We will try respond to your report within 7 days with our evaluation of the report and an expected resolution date. 23 | - We will handle your report with strict confidentiality, and not pass on your personal details to third parties without your permission. 24 | - We will keep you informed of the progress towards resolving the problem. -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | //import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | //import 'package:vartalap/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | // await tester.pumpWidget(Home("Vartalap")); 17 | 18 | // // Verify that our counter starts at 0. 19 | // expect(find.text('0'), findsOneWidget); 20 | // expect(find.text('1'), findsNothing); 21 | 22 | // // Tap the '+' icon and trigger a frame. 23 | // await tester.tap(find.byIcon(Icons.add)); 24 | // await tester.pump(); 25 | 26 | // // Verify that our counter has incremented. 27 | // expect(find.text('0'), findsNothing); 28 | // expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | Vartalap 18 | 19 | 20 | 21 | 24 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /lib/utils/remote_message_helper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:vartalap/models/remoteMessage.dart'; 4 | import 'package:vartalap/services/crashlystics.dart'; 5 | 6 | List toRemoteMessage(dynamic event) { 7 | List _messages = []; 8 | if (event is List) { 9 | for (var e in event) { 10 | _messages.addAll(toRemoteMessage(e)); 11 | } 12 | return _messages; 13 | } 14 | dynamic incomming = json.decode(event); 15 | 16 | if (incomming is Map) { 17 | try { 18 | var message = RemoteMessage.fromMap(incomming as Map); 19 | _messages.add(message); 20 | } catch (ex, stack) { 21 | Crashlytics.recordError(ex, stack, 22 | reason: "Exception while decoding server msg"); 23 | } 24 | } else if (incomming is List) { 25 | for (var msg in incomming) { 26 | if (msg is String) { 27 | _messages.addAll(toRemoteMessage(msg)); 28 | } else if (msg is Map) { 29 | try { 30 | _messages.add(RemoteMessage.fromMap(msg as Map)); 31 | } catch (e, stack) { 32 | Crashlytics.recordError(e, stack, 33 | reason: 34 | "Exception while decoding server msg : ${msg.toString()}"); 35 | } 36 | } 37 | } 38 | } 39 | return _messages; 40 | } 41 | -------------------------------------------------------------------------------- /lib/utils/dateTimeFormat.dart: -------------------------------------------------------------------------------- 1 | final List _months = [ 2 | "", 3 | "Jan", 4 | "Feb", 5 | "Mar", 6 | "Apr", 7 | "May", 8 | "June", 9 | "July", 10 | "Aug", 11 | "Sept", 12 | "Oct", 13 | "Nov", 14 | "Dec" 15 | ]; 16 | String formatMessageDate(int timestamp) { 17 | final date = DateTime.fromMillisecondsSinceEpoch(timestamp); 18 | final format = (int n) => n < 10 ? "0$n" : n; 19 | final today = DateTime.now(); 20 | if (date.year == today.year && date.month == today.month) { 21 | if (date.day == today.day) { 22 | return "Today"; 23 | } else if (date.day == today.day - 1) { 24 | return "Yesterday"; 25 | } 26 | } 27 | if (date.year == today.year) { 28 | return "${_months[date.month]} ${date.day}"; 29 | } 30 | return "${_months[date.month]} ${date.day}, ${format(date.year)}"; 31 | } 32 | 33 | String formatMessageTime(int timestamp) { 34 | var date = DateTime.fromMillisecondsSinceEpoch(timestamp); 35 | var format = (int n) => n < 10 ? "0$n" : n; 36 | return "${date.hour}:${format(date.minute)}"; 37 | } 38 | 39 | String formatMessageTimestamp(int timestamp) { 40 | final date = DateTime.fromMillisecondsSinceEpoch(timestamp); 41 | final today = DateTime.now(); 42 | if (date.year == today.year && 43 | date.day == today.day && 44 | date.month == today.month) { 45 | return formatMessageTime(timestamp); 46 | } 47 | return formatMessageDate(timestamp); 48 | } 49 | -------------------------------------------------------------------------------- /lib/widgets/loadingIndicator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class LoadingIndicator extends StatelessWidget { 4 | LoadingIndicator({this.text = ''}); 5 | 6 | final String text; 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | var displayedText = text; 11 | 12 | return Container( 13 | padding: EdgeInsets.all(16), 14 | child: Column( 15 | mainAxisAlignment: MainAxisAlignment.center, 16 | mainAxisSize: MainAxisSize.min, 17 | children: [ 18 | _getLoadingIndicator(), 19 | _getHeading(context), 20 | _getText(displayedText) 21 | ])); 22 | } 23 | 24 | Padding _getLoadingIndicator() { 25 | return Padding( 26 | child: Container( 27 | child: CircularProgressIndicator(strokeWidth: 3), 28 | width: 32, 29 | height: 32), 30 | padding: EdgeInsets.only(bottom: 16)); 31 | } 32 | 33 | Widget _getHeading(context) { 34 | return Padding( 35 | child: Text( 36 | 'Please wait …', 37 | style: TextStyle(fontSize: 16), 38 | textAlign: TextAlign.center, 39 | ), 40 | padding: EdgeInsets.only(bottom: 4)); 41 | } 42 | 43 | Text _getText(String displayedText) { 44 | return Text( 45 | displayedText, 46 | style: TextStyle(fontSize: 14), 47 | textAlign: TextAlign.center, 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/services/crashlystics.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 3 | 4 | class Crashlytics { 5 | static FirebaseCrashlytics _crashlytics = FirebaseCrashlytics.instance; 6 | 7 | static init() { 8 | _crashlytics.setCrashlyticsCollectionEnabled(kReleaseMode); 9 | Function? originalOnError = FlutterError.onError; 10 | FlutterError.onError = (FlutterErrorDetails errorDetails) async { 11 | await FirebaseCrashlytics.instance.recordFlutterError(errorDetails); 12 | // Forward to original handler. 13 | originalOnError!(errorDetails); 14 | }; 15 | } 16 | 17 | static Future recordError( 18 | dynamic exception, 19 | StackTrace stack, { 20 | dynamic reason, 21 | Iterable information = const [], 22 | }) async { 23 | return _crashlytics.recordError(exception, stack, 24 | reason: reason, information: information, printDetails: false); 25 | } 26 | 27 | static Future recordFlutterError( 28 | FlutterErrorDetails flutterErrorDetails) { 29 | return _crashlytics.recordFlutterError(flutterErrorDetails); 30 | } 31 | 32 | static Future log(String message) async { 33 | return _crashlytics.log(message); 34 | } 35 | 36 | /// The value can only be a type [int], [num], [String] or [bool]. 37 | static Future setCustomKey(String key, dynamic value) async { 38 | return _crashlytics.setCustomKey(key, value); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | I'm really glad you're reading this, because we need volunteer developers to help this project grow. 4 | 5 | 6 | #### **Did you find a bug?** 7 | 8 | * **Do not open up a GitHub issue if the bug is a security vulnerability**, and instead to refer to our [security policy](./SECURITY.md). 9 | 10 | * **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/ramank775/vartalap/issues). 11 | 12 | * If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/ramank775/vartalap/issues/new). 13 | Be sure to include a **title and clear description**, as much relevant information as possible. 14 | 15 | #### **Did you write a patch that fixes a bug?** 16 | 17 | * Open a new GitHub pull request with the patch. 18 | 19 | * Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. 20 | 21 | #### **Do you intend to add a new feature or change an existing one?** 22 | 23 | * Suggest your change in [Ideas in discussion](https://github.com/ramank775/vartalap/discussions/categories/ideas). 24 | * Once approved [open a new issue](https://github.com/ramank775/vartalap/issues/new) and start writing code. 25 | 26 | #### **Do you have questions?** 27 | 28 | * Ask any question about the project on [Q&A in discussion](https://github.com/ramank775/vartalap/discussions/categories/q-a). 29 | 30 | Thanks! :heart: :heart: :heart: 31 | 32 | Raman 33 | -------------------------------------------------------------------------------- /lib/config/config_store.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/services.dart' show rootBundle; 5 | import 'package:package_info_plus/package_info_plus.dart'; 6 | 7 | class ConfigStore { 8 | static ConfigStore _singleTon = ConfigStore._internal(); 9 | static String _configFile = kDebugMode ? "config.local.json" : "config.json"; 10 | static String _licenseFile = "LICENCE"; 11 | static bool _isloaded = false; 12 | factory ConfigStore() { 13 | return _singleTon; 14 | } 15 | 16 | Map _appConfig = Map(); 17 | PackageInfo packageInfo = PackageInfo( 18 | appName: 'Vartalap', 19 | packageName: 'com.one9x.vartalap', 20 | buildNumber: '', 21 | version: '', 22 | ); 23 | 24 | String subtitle = "Open source personal chat messager"; 25 | 26 | ConfigStore._internal(); 27 | 28 | Future loadConfig() async { 29 | if (_isloaded) return; 30 | String rawContent = await rootBundle.loadString(_configFile); 31 | Map content = 32 | Map.from(json.decode(rawContent)); 33 | _appConfig.addAll(content); 34 | packageInfo = await PackageInfo.fromPlatform(); 35 | 36 | LicenseRegistry.addLicense(() async* { 37 | var license = await rootBundle.loadString(_licenseFile); 38 | yield LicenseEntryWithLineBreaks([packageInfo.appName], license); 39 | }); 40 | _isloaded = true; 41 | } 42 | 43 | T get(String key) { 44 | return _appConfig[key]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/widgets/Inherited/auth_listener.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:vartalap/screens/login/introduction.dart'; 5 | import 'package:vartalap/screens/startup/startup.dart'; 6 | import 'package:vartalap/services/auth_service.dart'; 7 | import 'package:vartalap/services/user_service.dart'; 8 | import 'package:vartalap/widgets/Inherited/current_user.dart'; 9 | 10 | class AuthListner extends StatefulWidget { 11 | final MaterialApp app; 12 | const AuthListner({ 13 | Key? key, 14 | required this.app, 15 | }) : super(key: key); 16 | 17 | @override 18 | State createState() => _AuthListnerState(); 19 | } 20 | 21 | class _AuthListnerState extends State { 22 | final authService = AuthService.instance; 23 | 24 | late StreamSubscription _sub; 25 | late GlobalKey _navigatorKey; 26 | late bool _isLogin; 27 | @override 28 | void initState() { 29 | super.initState(); 30 | _isLogin = authService.isLoggedIn(); 31 | _navigatorKey = widget.app.navigatorKey!; 32 | _sub = authService.authStateChange.listen((event) { 33 | setState(() { 34 | this._isLogin = authService.isLoggedIn(); 35 | }); 36 | this._navigatorKey.currentState!.pushAndRemoveUntil( 37 | MaterialPageRoute( 38 | builder: (ctx) => 39 | this._isLogin ? StartupScreen() : IntroductionScreen(), 40 | ), 41 | (route) => false); 42 | }); 43 | } 44 | 45 | @override 46 | Widget build(BuildContext context) { 47 | return CurrentUser( 48 | user: this._isLogin ? UserService.getLoggedInUser() : null, 49 | child: widget.app, 50 | ); 51 | } 52 | 53 | @override 54 | void dispose() { 55 | _sub.cancel(); 56 | super.dispose(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/models/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:vartalap/utils/enum_helper.dart'; 3 | 4 | enum UserStatus { 5 | ACTIVE, 6 | DELETED, 7 | UNKNOWN, 8 | OTHER, 9 | } 10 | 11 | class User { 12 | late String _name; 13 | late String _username; 14 | String? _pic; 15 | UserStatus status = UserStatus.ACTIVE; 16 | bool hasAccount = false; 17 | String get name => _name; 18 | String get username => _username; 19 | String? get pic => _pic; 20 | 21 | User(this._name, this._username, this._pic, 22 | {this.status = UserStatus.ACTIVE, this.hasAccount = false}); 23 | User.fromMap(Map map) { 24 | this._name = map["name"]; 25 | this._username = map["username"]; 26 | this._pic = map["pic"]; 27 | this.hasAccount = (map["hasAccount"] ?? 0) == 1; 28 | this.status = map.containsKey('status') 29 | ? intToEnum(map['status'], UserStatus.values) 30 | : UserStatus.ACTIVE; 31 | } 32 | Map toMap({bool persistent = false}) { 33 | Map map = Map(); 34 | map["username"] = this.username; 35 | map["name"] = this.name; 36 | map["pic"] = this.pic; 37 | map["hasAccount"] = this.hasAccount; 38 | map["status"] = enumToInt(this.status, UserStatus.values); 39 | return map; 40 | } 41 | 42 | int get hashCode => "user_$username".hashCode; 43 | 44 | @override 45 | bool operator ==(Object other) { 46 | return hashCode == other.hashCode; 47 | } 48 | } 49 | 50 | class UserNotifier extends ValueNotifier { 51 | late User _value; 52 | UserNotifier(User value) : super(value) { 53 | this._value = value; 54 | } 55 | 56 | @override 57 | User get value => _value; 58 | 59 | void update(User value) { 60 | this._value = value; 61 | this.notifyListeners(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | Vartalap 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | NSContactsUsageDescription 44 | This app requires contacts access to function properly. 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /lib/widgets/contact.dart: -------------------------------------------------------------------------------- 1 | import 'package:vartalap/models/user.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:vartalap/theme/theme.dart'; 4 | import 'package:vartalap/widgets/avator.dart'; 5 | 6 | class ContactItem extends StatelessWidget { 7 | final User user; 8 | final Function? onProfileTap; 9 | final Function? onTap; 10 | final bool isSelected; 11 | final bool enabled; 12 | ContactItem({ 13 | required this.user, 14 | this.isSelected = false, 15 | this.onProfileTap, 16 | this.onTap, 17 | this.enabled = true, 18 | }); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | final vtheme = VartalapTheme.theme; 23 | return ListTileTheme( 24 | selectedColor: vtheme.selectedRowColor, 25 | child: ListTile( 26 | contentPadding: EdgeInsets.symmetric( 27 | vertical: 2.0, 28 | horizontal: 16.0, 29 | ), 30 | leading: Container( 31 | width: 45, 32 | height: 45, 33 | child: Stack( 34 | children: [ 35 | Avator( 36 | width: 45.0, 37 | height: 45.0, 38 | text: user.name, 39 | ), 40 | this.isSelected 41 | ? Positioned( 42 | bottom: 0, 43 | right: 0, 44 | child: CircleAvatar( 45 | backgroundColor: Theme.of(context).primaryColor, 46 | radius: 10, 47 | child: Icon( 48 | Icons.check, 49 | size: 15, 50 | color: Colors.white, 51 | ), 52 | ), 53 | ) 54 | : Container() 55 | ], 56 | ), 57 | ), 58 | title: Text( 59 | user.name, 60 | maxLines: 1, 61 | style: TextStyle( 62 | fontSize: 18.0, 63 | fontWeight: FontWeight.bold, 64 | ), 65 | ), 66 | subtitle: Text( 67 | user.username, 68 | maxLines: 1, 69 | ), 70 | onTap: () { 71 | if (onTap != null) { 72 | onTap!(user); 73 | } 74 | }, 75 | selected: isSelected, 76 | enabled: enabled, 77 | ), 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /lib/services/performance_metric.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_performance/firebase_performance.dart'; 2 | import 'package:vartalap/utils/enum_helper.dart'; 3 | 4 | class PerformanceTrace { 5 | final Trace _trace; 6 | PerformanceTrace(this._trace); 7 | 8 | Future start() { 9 | return _trace.start(); 10 | } 11 | 12 | Future stop() { 13 | return _trace.stop(); 14 | } 15 | 16 | void putAttribute(String name, dynamic value) { 17 | return _trace.putAttribute(name, value.toString()); 18 | } 19 | 20 | void setMetric(String name, int value) { 21 | return _trace.setMetric(name, value); 22 | } 23 | 24 | void incrementMetric(String name) { 25 | return _trace.incrementMetric(name, 1); 26 | } 27 | } 28 | 29 | class HttpPerformanceTrace { 30 | final HttpMetric _metric; 31 | 32 | HttpPerformanceTrace(this._metric); 33 | 34 | Future start() { 35 | return _metric.start(); 36 | } 37 | 38 | Future stop() { 39 | return _metric.stop(); 40 | } 41 | 42 | int get httpResponseCode => _metric.httpResponseCode!; 43 | 44 | int get requestPayloadSize => _metric.requestPayloadSize ?? 0; 45 | 46 | String get responseContentType => _metric.responseContentType ?? ''; 47 | 48 | int get responsePayloadSize => _metric.responsePayloadSize ?? 0; 49 | 50 | set httpResponseCode(int httpResponseCode) { 51 | _metric.httpResponseCode = httpResponseCode; 52 | } 53 | 54 | set requestPayloadSize(int requestPayloadSize) { 55 | _metric.requestPayloadSize = requestPayloadSize; 56 | } 57 | 58 | set responseContentType(String responseContentType) { 59 | _metric.responseContentType = responseContentType; 60 | } 61 | 62 | set responsePayloadSize(int responsePayloadSize) { 63 | _metric.responsePayloadSize = responsePayloadSize; 64 | } 65 | } 66 | 67 | class PerformanceMetric { 68 | static FirebasePerformance _firebasePerformance = 69 | FirebasePerformance.instance; 70 | 71 | static init() { 72 | _firebasePerformance.setPerformanceCollectionEnabled(true); 73 | } 74 | 75 | static PerformanceTrace newTrace(String name) { 76 | return PerformanceTrace(_firebasePerformance.newTrace(name)); 77 | } 78 | 79 | static HttpPerformanceTrace newHttpMetric(String url, String method) { 80 | var httpMethod = stringToEnum(method, HttpMethod.values); 81 | return HttpPerformanceTrace( 82 | _firebasePerformance.newHttpMetric(url, httpMethod)); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/models/chat.dart: -------------------------------------------------------------------------------- 1 | import 'package:vartalap/utils/enum_helper.dart'; 2 | 3 | import 'package:vartalap/models/user.dart'; 4 | 5 | enum ChatType { 6 | NONE, 7 | INDIVIDUAL, 8 | GROUP, 9 | OTHER, 10 | } 11 | 12 | enum UserRole { 13 | USER, 14 | ADMIN, 15 | EX_USER, 16 | OTHER, 17 | } 18 | 19 | class ChatUser extends User { 20 | UserRole role = UserRole.USER; 21 | ChatUser(String name, String username, String? pic, 22 | [this.role = UserRole.USER]) 23 | : super(name, username, pic); 24 | 25 | ChatUser.fromMap(Map map) : super.fromMap(map) { 26 | this.role = intToEnum(map["role"], UserRole.values); 27 | } 28 | 29 | ChatUser.fromUser(User user) : super(user.name, user.username, user.pic); 30 | 31 | Map toMap({bool persistent = false}) { 32 | Map map = super.toMap(persistent: persistent); 33 | if (persistent) return map; 34 | map["name"] = this.name; 35 | map["username"] = this.username; 36 | map["pic"] = this.pic; 37 | map["role"] = enumToInt(this.role, UserRole.values); 38 | return map; 39 | } 40 | 41 | int get hashCode => "user_$username".hashCode; 42 | 43 | @override 44 | bool operator ==(Object other) { 45 | return hashCode == other.hashCode; 46 | } 47 | } 48 | 49 | class Chat { 50 | late String _id; 51 | late String _title; 52 | String? _pic; 53 | ChatType type = ChatType.INDIVIDUAL; 54 | Set _users = new Set(); 55 | 56 | Chat(this._id, this._title, this._pic, {this.type = ChatType.INDIVIDUAL}); 57 | 58 | String get id => _id; 59 | String get title => _title; 60 | String? get pic => _pic; 61 | List get users => _users.toList(); 62 | 63 | Chat.fromMap(Map map) { 64 | this._id = map["id"]; 65 | this._title = map["title"]; 66 | this._pic = map["pic"]; 67 | this.type = intToEnum(map["type"] ?? 1, ChatType.values); 68 | } 69 | 70 | Map toMap() { 71 | Map map = Map(); 72 | map["id"] = this.id; 73 | map["title"] = this.title; 74 | map["pic"] = this.pic; 75 | map["type"] = enumToInt(this.type, ChatType.values); 76 | return map; 77 | } 78 | 79 | int get hashCode => "chat_$id".hashCode; 80 | 81 | void addUser(ChatUser user) { 82 | this._users.add(user); 83 | } 84 | 85 | void resetUsers() { 86 | this._users.clear(); 87 | } 88 | 89 | @override 90 | bool operator ==(Object other) { 91 | return hashCode == other.hashCode; 92 | } 93 | } 94 | 95 | class ChatPreview extends Chat { 96 | String _content = ''; 97 | int _ts = 0; 98 | int _unread = 0; 99 | ChatPreview(String id, String title, String? pic, this._content, this._ts, 100 | this._unread) 101 | : super(id, title, pic); 102 | 103 | ChatPreview.fromMap(Map map) : super.fromMap(map) { 104 | this._content = map["text"] ?? ''; 105 | this._ts = map["ts"] ?? 0; 106 | this._unread = map["unread"] == null ? 0 : map["unread"]; 107 | } 108 | 109 | String get content => this._content; 110 | int get ts => this._ts; 111 | int get unread => this._unread; 112 | } 113 | -------------------------------------------------------------------------------- /lib/widgets/avator_letter.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | enum LetterType { Rectangle, Circular, None } 4 | 5 | Color parseColor({required String hexCode}) { 6 | String hex = hexCode.replaceAll("#", ""); 7 | if (hex.isEmpty) hex = "000000"; 8 | if (hex.length == 3) { 9 | hex = 10 | '${hex.substring(0, 1)}${hex.substring(0, 1)}${hex.substring(1, 2)}${hex.substring(1, 2)}${hex.substring(2, 3)}${hex.substring(2, 3)}'; 11 | } 12 | Color col = Color(int.parse(hex, radix: 16)); 13 | return col; 14 | } 15 | 16 | @immutable 17 | class AvatarLetter extends StatelessWidget { 18 | final LetterType letterType; 19 | final String text; 20 | final double fontSize; 21 | final FontWeight fontWeight; 22 | final String? fontFamily; 23 | final Color? backgroundColor; 24 | final Color? textColor; 25 | final double? size; 26 | final int numberLetters; 27 | final bool upperCase; 28 | 29 | AvatarLetter( 30 | {Key? key, 31 | this.letterType = LetterType.Rectangle, 32 | required this.text, 33 | required this.textColor, 34 | required this.backgroundColor, 35 | this.size = 50.0, 36 | this.numberLetters = 1, 37 | this.fontWeight = FontWeight.bold, 38 | this.fontFamily, 39 | this.fontSize = 16, 40 | this.upperCase = false}) { 41 | assert(numberLetters > 0); 42 | } 43 | 44 | @override 45 | Widget build(BuildContext context) { 46 | return _leeterView(); 47 | } 48 | 49 | String _runeString({required String value}) { 50 | return String.fromCharCodes(value.runes.toList()); 51 | } 52 | 53 | String _textConfig() { 54 | var newText = _runeString(value: text); 55 | newText = upperCase ? newText.toUpperCase() : newText; 56 | var arrayLeeters = newText.trim().split(' '); 57 | if (arrayLeeters.length > 1 && arrayLeeters.length == numberLetters) { 58 | return '${arrayLeeters[0][0].trim()}${arrayLeeters[1][0].trim()}'; 59 | } 60 | return '${newText[0]}'; 61 | } 62 | 63 | Widget _buildText() { 64 | return Text( 65 | _textConfig(), 66 | style: TextStyle( 67 | color: textColor, 68 | fontSize: fontSize, 69 | fontWeight: fontWeight, 70 | fontFamily: fontFamily, 71 | ), 72 | ); 73 | } 74 | 75 | _buildTypeLeeter() { 76 | switch (letterType) { 77 | case LetterType.Rectangle: 78 | return RoundedRectangleBorder( 79 | borderRadius: BorderRadius.circular(5.0), 80 | ); 81 | case LetterType.Circular: 82 | return RoundedRectangleBorder( 83 | borderRadius: BorderRadius.circular(size! / 2), 84 | ); 85 | case LetterType.None: 86 | return RoundedRectangleBorder( 87 | borderRadius: BorderRadius.circular(0.0), 88 | ); 89 | } 90 | } 91 | 92 | Widget _leeterView() { 93 | return Container( 94 | child: Material( 95 | shape: _buildTypeLeeter(), 96 | color: backgroundColor, 97 | child: Container( 98 | height: size, 99 | width: size, 100 | child: Center( 101 | child: _buildText(), 102 | ), 103 | ), 104 | ), 105 | ); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/utils/color_helper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:vartalap/utils/random_color.dart'; 4 | 5 | MaterialColor generateMaterialColor(Color color) { 6 | return MaterialColor(color.value, { 7 | 50: tintColor(color, 0.9), 8 | 100: tintColor(color, 0.8), 9 | 200: tintColor(color, 0.6), 10 | 300: tintColor(color, 0.4), 11 | 400: tintColor(color, 0.2), 12 | 500: color, 13 | 600: shadeColor(color, 0.1), 14 | 700: shadeColor(color, 0.2), 15 | 800: shadeColor(color, 0.3), 16 | 900: shadeColor(color, 0.4), 17 | }); 18 | } 19 | 20 | int tintValue(int value, double factor) => 21 | max(0, min((value + ((255 - value) * factor)).round(), 255)); 22 | 23 | Color tintColor(Color color, double factor) => Color.fromRGBO( 24 | tintValue(color.red, factor), 25 | tintValue(color.green, factor), 26 | tintValue(color.blue, factor), 27 | 1); 28 | 29 | int shadeValue(int value, double factor) => 30 | max(0, min(value - (value * factor).round(), 255)); 31 | 32 | Color shadeColor(Color color, double factor) => Color.fromRGBO( 33 | shadeValue(color.red, factor), 34 | shadeValue(color.green, factor), 35 | shadeValue(color.blue, factor), 36 | 1); 37 | 38 | class Range { 39 | const Range(this.start, this.end); 40 | 41 | const Range.staticValue(int value) 42 | : start = value, 43 | end = value; 44 | const Range.zero() 45 | : start = 0, 46 | end = 0; 47 | 48 | final int start; 49 | final int end; 50 | 51 | Range operator +(Range range) { 52 | return Range((start + range.start) ~/ 2, end); 53 | } 54 | 55 | bool contain(int value) { 56 | return value >= start && value <= end; 57 | } 58 | 59 | int randomWithin(Random random) { 60 | return (start + random.nextDouble() * (end - start)).round(); 61 | } 62 | 63 | @override 64 | bool operator ==(Object other) => 65 | identical(this, other) || 66 | other is Range && 67 | runtimeType == other.runtimeType && 68 | start == other.start && 69 | end == other.end; 70 | 71 | @override 72 | int get hashCode => start.hashCode ^ end.hashCode; 73 | } 74 | 75 | Color getColor( 76 | String text, { 77 | double opacity = 1, 78 | Brightness brightness = Brightness.light, 79 | }) { 80 | var hash = 0; 81 | if (text.length == 0) return Colors.amber; 82 | for (var i = 0; i < text.length; i++) { 83 | hash = text.codeUnitAt(i) + ((hash << 5) - hash); 84 | hash = hash & hash; 85 | } 86 | final _colorGenerator = RandomColor(hash); 87 | final colorBrightness = brightness == Brightness.dark 88 | ? ColorBrightness.light 89 | : ColorBrightness.primary; 90 | final colorSaturation = brightness == Brightness.dark 91 | ? ColorSaturation.mediumSaturation 92 | : ColorSaturation.mediumSaturation; 93 | final color = _colorGenerator.randomColor( 94 | colorBrightness: colorBrightness, 95 | colorSaturation: colorSaturation, 96 | ); 97 | return color.withOpacity(opacity); 98 | } 99 | 100 | /// Construct a color from a hex code string, of the format #RRGGBB. 101 | Color hexToColor(String code) { 102 | return new Color(int.parse(code.substring(1, 7), radix: 16) + 0xFF000000); 103 | } 104 | -------------------------------------------------------------------------------- /lib/dataAccessLayer/db.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'package:path/path.dart'; 3 | import 'package:sqflite/sqflite.dart'; 4 | 5 | class DB { 6 | final dbName = "Chat"; 7 | final version = 4; 8 | late Future _db = initDatabase(); 9 | 10 | static final DB _singleton = DB._internal(); 11 | 12 | factory DB() { 13 | return _singleton; 14 | } 15 | 16 | DB._internal(); 17 | 18 | Future _getDatabasePath(String dbName) async { 19 | final databasePath = await getDatabasesPath(); 20 | final path = join(databasePath, dbName); 21 | 22 | final dbDirectory = Directory(dirname(path)); 23 | final isDirectoryExists = await dbDirectory.exists(); 24 | if (!isDirectoryExists) { 25 | await dbDirectory.create(recursive: true); 26 | } 27 | return path; 28 | } 29 | 30 | Future initDatabase() async { 31 | final path = await _getDatabasePath(dbName); 32 | var db = await openDatabase( 33 | path, 34 | version: version, 35 | onCreate: _onCreate, 36 | onUpgrade: _onUpgrade, 37 | ); 38 | return db; 39 | } 40 | 41 | Future _onCreate(Database db, dynamic version) async { 42 | var batch = db.batch(); 43 | batch.execute("PRAGMA foreign_keys = ON;"); 44 | batch.execute("""CREATE TABLE user ( 45 | username TEXT PRIMARY KEY, 46 | name TEXT, 47 | pic TEXT, 48 | hasAccount NUMBER, 49 | status NUMBER DEFAULT 0 50 | );"""); 51 | 52 | batch.execute("""CREATE TABLE chat ( 53 | id TEXT PRIMARY KEY, 54 | title TEXT, 55 | pic TEXT, 56 | type NUMBER DEFAULT 1, 57 | createdOn int 58 | );"""); 59 | 60 | batch.execute("""CREATE TABLE chat_user ( 61 | id INTEGER PRIMARY KEY AUTOINCREMENT, 62 | userid TEXT, 63 | chatid TEXT, 64 | role NUMBER DEFAULT 1, 65 | FOREIGN KEY(chatid) REFERENCES chat(id), 66 | FOREIGN KEY(userid) REFERENCES user(username) 67 | );"""); 68 | 69 | batch.execute("""CREATE TABLE message ( 70 | id TEXT PRIMARY KEY, 71 | chatid TEXT, 72 | senderid TEXT, 73 | text TEXT, 74 | state NUMBER DEFAULT 1, 75 | type NUMBER DEFAULT 1, 76 | ts NUMBER, 77 | FOREIGN KEY(chatid) REFERENCES chat(id), 78 | FOREIGN KEY(senderid) REFERENCES user(username) 79 | );"""); 80 | 81 | batch.execute("""CREATE TABLE out_message ( 82 | id int PRIMARY KEY, 83 | messageId TEXT, 84 | message TEXT, 85 | sent NUMBER, 86 | created_ts NUMBER, 87 | sent_ts NUMBER, 88 | retry_count NUMBER DEFAULT 0 89 | );"""); 90 | batch.commit(); 91 | } 92 | 93 | Future _onUpgrade(Database db, int current, int next) async { 94 | if (next == 2) { 95 | await db.execute(""" 96 | ALTER TABLE user 97 | ADD status NUMBER DEFAULT 0; 98 | """); 99 | } else if (next == 3) { 100 | await db.execute(""" 101 | ALTER TABLE message 102 | ADD body TEXT; 103 | """); 104 | } else if (next == 4) { 105 | await db.delete( 106 | "out_message", 107 | where: "sent != ?", 108 | whereArgs: [-1], 109 | ); 110 | } 111 | } 112 | 113 | Future getDb() async { 114 | return _db; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | def keystoreProperties = new Properties() 29 | def keystorePropertiesFile = rootProject.file('key.properties') 30 | if(keystorePropertiesFile.exists()) { 31 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) 32 | } 33 | 34 | android { 35 | namespace "com.one9x.vartalap" 36 | compileSdk 33 37 | 38 | compileOptions { 39 | sourceCompatibility JavaVersion.VERSION_1_8 40 | targetCompatibility JavaVersion.VERSION_1_8 41 | } 42 | 43 | kotlinOptions { 44 | jvmTarget = '1.8' 45 | } 46 | 47 | sourceSets { 48 | main.java.srcDirs += 'src/main/kotlin' 49 | } 50 | 51 | 52 | defaultConfig { 53 | applicationId "com.one9x.vartalap" 54 | minSdkVersion 19 55 | targetSdkVersion 33 56 | versionCode flutterVersionCode.toInteger() 57 | versionName flutterVersionName 58 | multiDexEnabled true 59 | 60 | } 61 | 62 | signingConfigs { 63 | release { 64 | keyAlias keystoreProperties['keyAlias'] 65 | keyPassword keystoreProperties['keyPassword'] 66 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null 67 | storePassword keystoreProperties['storePassword'] 68 | } 69 | } 70 | 71 | buildTypes { 72 | release { 73 | signingConfig signingConfigs.release 74 | } 75 | 76 | debug { 77 | signingConfig signingConfigs.release 78 | } 79 | } 80 | 81 | flavorDimensions += "app" 82 | 83 | productFlavors { 84 | 85 | dev { 86 | dimension "app" 87 | resValue "string", "app_name", "Vartalap Dev" 88 | versionNameSuffix "-dev" 89 | applicationId "com.one9x.vartalap.dev" 90 | } 91 | 92 | prod { 93 | dimension "app" 94 | resValue "string", "app_name", "Vartalap" 95 | } 96 | } 97 | lint { 98 | disable 'InvalidPackage' 99 | } 100 | } 101 | 102 | flutter { 103 | source '../..' 104 | } 105 | 106 | dependencies { 107 | implementation 'androidx.browser:browser:1.4.0' 108 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 109 | } 110 | 111 | apply plugin: 'com.google.gms.google-services' 112 | apply plugin: 'com.google.firebase.firebase-perf' 113 | apply plugin: 'com.google.firebase.crashlytics' 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vartalap 2 | 3 | [Vartalap](https://vartalap.one9x.org) is an open source personal chat application. It is design to provide the level of transparency in the personal messaging application with your data. 4 | 5 | ## Supported Platform 6 | 7 | - [x] Android 8 | - [ ] Ios 9 | 10 | ## Features 11 | - Texts with emoji 12 | - Group Chat 13 | 14 | ## Setup 15 | 16 | ### Local setup 17 | 18 | - Download or clone the repo `https://github.com/ramank775/vartalap.git` 19 | - Get the required dependencies `flutter pub get` 20 | 21 | - Setup local chat-sever by following instruction in [chat-server](https://www.github.com/ramank775/chat-server) repo. 22 | - Create copy of `config.json.tmpl` to `config.local.json` (for development setup) and `config.json` (for production build). 23 | - Update `api_url` and `ws_url` of [chat-server](https://www.github.com/ramank775/chat-server) 24 | 25 | 26 | ### Setup with Gitpod 27 | Click on the Gitpod badge to start cloud IDE 28 | 29 | [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/ramank775/vartalap) 30 | 31 | Localhost command 32 | - Feel free to use your own ports configuration 33 | 34 | `SMARTPHONE_INTERNAL_IP = 192.168.0.10` 35 | 36 | `SMARTPHONE_INTERNAL_PORT = 5555` 37 | 38 | - To switch adb on your device to work over the network using port 5555 39 | 40 | `adb tcpip SMARTPHONE_INTERNAL_PORT` 41 | 42 | - Check connection from localhost 43 | 44 | `adb connect SMARTPHONE_INTERNAL_IP:SMARTPHONE_INTERNAL_PORT` 45 | 46 | - Ngrok tcp forward to your mobile or Forward a chosen port on your router 47 | 48 | `ngrok tcp SMARTPHONE_INTERNAL_IP:SMARTPHONE_INTERNAL_PORT` 49 | 50 | Gitpod command 51 | - Connect from your Gitpod to your localhost for debugging 52 | `adb connect NGROK_ADDRESS:NGROK_PORT` 53 | 54 | `flutter run` 55 | 56 | Chat Server 57 | - Start chat-sever by following instruction in [chat-server](https://www.github.com/ramank775/chat-server) repo. 58 | - Create copy of `config.json.tmpl` to `config.local.json` (for development setup) and `config.json` (for production build). 59 | - Update `api_url` and `ws_url` of [chat-server](https://www.github.com/ramank775/chat-server) 60 | 61 | 62 | 63 | # Contribution 64 | [Vartalap](https://vartalap.one9x.org) is an open source project. We are looking for building the community around the project, welcoming everyone or anyone who is interested in contributing. 65 | 66 | - Facing any issue? Raise an issue [here](https://github.com/ramank775/vartalap/issues/new?assignees=&labels=bug&template=bug_report.md&title=%5BBUG%5D) with the necessary details. 67 | 68 | - Looking for a new feature? Raise an feature request [here](https://github.com/ramank775/vartalap/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=%5BFEAT%5D). 69 | 70 | - Found a security issue? Report it responsibility, view our security policy [here](https://github.com/ramank775/vartalap/security/policy). 71 | 72 | - Wants to resolve an issue? **Thanks!** initiate the discussion on issue of your choice. 73 | 74 | ## Code Of Conduct 75 | 76 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](./CODE_OF_CONDUCT.md) 77 | 78 | Vartalap has adopted [Contributor Covenant](./CODE_OF_CONDUCT.md), we expect project participants to adhere to. Please read the [full text](./CODE_OF_CONDUCT.md) to understand what action will and will not be tolerated. 79 | 80 | 81 | # LICENSE 82 | [GNU GENERAL PUBLIC LICENSE](./LICENCE) 83 | 84 | # Contact us 85 | - Twitter [@vartalap_app](https://twitter.com/vartalap_app). 86 | 87 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: vartalap 2 | description: Open source chat application 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `pub publish`. This is preferred for private packages. 6 | publish_to: "none" # Remove this line if you wish to publish to pub.dev 7 | 8 | # The following defines the version and build number for your application. 9 | # A version number is three numbers separated by dots, like 1.2.43 10 | # followed by an optional build number separated by a +. 11 | # Both the version and the builder number may be overridden in flutter 12 | # build by specifying --build-name and --build-number, respectively. 13 | # In Android, build-name is used as versionName while build-number used as versionCode. 14 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 15 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 16 | # Read more about iOS versioning at 17 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 18 | version: 2.5.20+79 19 | 20 | environment: 21 | sdk: ">=3.0.0 <4.0.0" 22 | 23 | dependencies: 24 | flutter: 25 | sdk: flutter 26 | 27 | # The following adds the Cupertino Icons font to your application. 28 | # Use with the CupertinoIcons class for iOS style icons. 29 | cupertino_icons: ^1.0.5 30 | sqflite: ^2.3.0 31 | flutter_contacts: ^1.1.7+1 32 | permission_handler: ^10.4.3 33 | emoji_picker_flutter: ^1.6.1 34 | flutter_secure_storage: ^8.0.0 35 | firebase_core: ^2.15.1 36 | firebase_auth: ^4.7.3 37 | firebase_messaging: ^14.6.6 38 | http: ^1.1.0 39 | connectivity_plus: ^4.0.2 40 | package_info_plus: ^4.1.0 41 | flutter_local_notifications: ^15.1.0+1 42 | firebase_performance: ^0.9.2+5 43 | firebase_crashlytics: ^3.3.5 44 | url_launcher: ^6.1.12 45 | share_plus: ^7.1.0 46 | crypto: ^3.0.3 47 | bubble: ^1.2.1 48 | firebase_app_check: ^0.1.5+2 49 | 50 | dev_dependencies: 51 | flutter_test: 52 | sdk: flutter 53 | 54 | # For information on the generic Dart part of this file, see the 55 | # following page: https://dart.dev/tools/pub/pubspec 56 | 57 | # The following section is specific to Flutter. 58 | flutter: 59 | # The following line ensures that the Material Icons font is 60 | # included with your application, so that you can use the icons in 61 | # the material Icons class. 62 | uses-material-design: true 63 | 64 | # To add assets to your application, add an assets section, like this: 65 | assets: 66 | - config.json 67 | - config.local.json 68 | - LICENCE 69 | 70 | # An image asset can refer to one or more resolution-specific "variants", see 71 | # https://flutter.dev/assets-and-images/#resolution-aware. 72 | 73 | # For details regarding adding assets from package dependencies, see 74 | # https://flutter.dev/assets-and-images/#from-packages 75 | 76 | # To add custom fonts to your application, add a fonts section here, 77 | # in this "flutter" section. Each entry in this list should have a 78 | # "family" key with the font family name, and a "fonts" key with a 79 | # list giving the asset and other descriptors for the font. For 80 | # example: 81 | # fonts: 82 | # - family: Schyler 83 | # fonts: 84 | # - asset: fonts/Schyler-Regular.ttf 85 | # - asset: fonts/Schyler-Italic.ttf 86 | # style: italic 87 | # - family: Trajan Pro 88 | # fonts: 89 | # - asset: fonts/TrajanPro.ttf 90 | # - asset: fonts/TrajanPro_Bold.ttf 91 | # weight: 700 92 | # 93 | # For details regarding fonts from package dependencies, 94 | # see https://flutter.dev/custom-fonts/#from-packages 95 | fonts: 96 | - family: sofia 97 | fonts: 98 | - asset: fonts/Sofia Pro Medium Az.otf 99 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 36 | 44 | 48 | 52 | 57 | 61 | 62 | 63 | 64 | 65 | 66 | 68 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /lib/widgets/keyboard.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | typedef KeyboardTapCallback = void Function(String text); 4 | 5 | class NumericKeyboard extends StatefulWidget { 6 | final Color textColor; 7 | final Icon? rightIcon; 8 | final Function()? rightButtonFn; 9 | final Icon? leftIcon; 10 | final Function()? leftButtonFn; 11 | final KeyboardTapCallback onKeyboardTap; 12 | final MainAxisAlignment mainAxisAlignment; 13 | 14 | NumericKeyboard({ 15 | Key? key, 16 | required this.onKeyboardTap, 17 | this.textColor = Colors.black, 18 | this.rightButtonFn, 19 | this.rightIcon, 20 | this.leftButtonFn, 21 | this.leftIcon, 22 | this.mainAxisAlignment = MainAxisAlignment.spaceEvenly, 23 | }) : super(key: key); 24 | 25 | @override 26 | State createState() { 27 | return _NumericKeyboardState(); 28 | } 29 | } 30 | 31 | class _NumericKeyboardState extends State { 32 | @override 33 | Widget build(BuildContext context) { 34 | return Container( 35 | padding: const EdgeInsets.only(left: 32, right: 32), 36 | alignment: Alignment.center, 37 | child: SingleChildScrollView( 38 | child: Column( 39 | mainAxisAlignment: MainAxisAlignment.start, 40 | mainAxisSize: MainAxisSize.max, 41 | children: [ 42 | ButtonBar( 43 | alignment: widget.mainAxisAlignment, 44 | children: [ 45 | _calcButton('1'), 46 | _calcButton('2'), 47 | _calcButton('3'), 48 | ], 49 | ), 50 | ButtonBar( 51 | alignment: widget.mainAxisAlignment, 52 | children: [ 53 | _calcButton('4'), 54 | _calcButton('5'), 55 | _calcButton('6'), 56 | ], 57 | ), 58 | ButtonBar( 59 | alignment: widget.mainAxisAlignment, 60 | children: [ 61 | _calcButton('7'), 62 | _calcButton('8'), 63 | _calcButton('9'), 64 | ], 65 | ), 66 | ButtonBar( 67 | alignment: widget.mainAxisAlignment, 68 | children: [ 69 | InkWell( 70 | borderRadius: BorderRadius.circular(45), 71 | onTap: widget.leftButtonFn, 72 | child: Container( 73 | alignment: Alignment.center, 74 | width: 50, 75 | height: 50, 76 | child: widget.leftIcon, 77 | ), 78 | ), 79 | _calcButton('0'), 80 | InkWell( 81 | borderRadius: BorderRadius.circular(45), 82 | onTap: widget.rightButtonFn, 83 | child: Container( 84 | alignment: Alignment.center, 85 | width: 50, 86 | height: 50, 87 | child: widget.rightIcon, 88 | ), 89 | ) 90 | ], 91 | ), 92 | ], 93 | ), 94 | ), 95 | ); 96 | } 97 | 98 | Widget _calcButton(String value) { 99 | return InkWell( 100 | borderRadius: BorderRadius.circular(45), 101 | onTap: () { 102 | widget.onKeyboardTap(value); 103 | }, 104 | child: Container( 105 | alignment: Alignment.center, 106 | width: 50, 107 | height: 50, 108 | child: Text( 109 | value, 110 | style: TextStyle( 111 | fontSize: 26, 112 | fontWeight: FontWeight.bold, 113 | color: widget.textColor), 114 | ), 115 | )); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /lib/widgets/rich_message.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/gestures.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:vartalap/theme/theme.dart'; 4 | import 'package:vartalap/utils/url_helper.dart'; 5 | 6 | class RichMessage extends StatelessWidget { 7 | static final RegExp emojiRegex = RegExp( 8 | r'(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]|[\uf000-\uffff])', 9 | ); 10 | static final RegExp hyperlinkRegex = RegExp( 11 | r"(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/|ws:\/\/|wss:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?", 12 | caseSensitive: false, 13 | multiLine: true, 14 | ); 15 | static final RegExp emailRegex = RegExp( 16 | r"[a-zA-Z0-9-_.]+@[a-zA-Z0-9-_.]+", 17 | caseSensitive: false, 18 | ); 19 | 20 | final TextStyle style; 21 | final String text; 22 | final bool selectable; 23 | RichMessage(this.text, this.style, {this.selectable = false}); 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | final txtSpan = TextSpan( 28 | children: generateMessageTextSpans(text), 29 | style: this.style, 30 | ); 31 | if (selectable) { 32 | return SelectableText.rich(txtSpan); 33 | } 34 | return RichText(text: txtSpan); 35 | } 36 | 37 | List generateMessageTextSpans(String text) { 38 | List spans = []; 39 | final TextStyle emojiStyle = style.copyWith( 40 | fontSize: (style.fontSize! * 1.5), 41 | letterSpacing: 0.5, 42 | ); 43 | 44 | final TextStyle combinedEmojiStyle = emojiStyle.copyWith( 45 | fontSize: style.fontSize! * 1.7, 46 | ); 47 | 48 | final TextStyle hyperLinkStyle = 49 | this.style.merge(VartalapTheme.theme.linkTitleStyle); 50 | String emojiString = ""; 51 | 52 | text.splitMapJoin( 53 | emojiRegex, 54 | onMatch: (m) { 55 | emojiString += m.group(0)!; 56 | return ""; 57 | }, 58 | onNonMatch: (s) { 59 | if (s.isEmpty) { 60 | return ""; 61 | } else if (emojiString.isNotEmpty) { 62 | spans.add( 63 | TextSpan( 64 | text: emojiString, 65 | style: emojiString.length > 1 ? combinedEmojiStyle : emojiStyle, 66 | ), 67 | ); 68 | emojiString = ""; 69 | } 70 | s.splitMapJoin( 71 | emailRegex, 72 | onMatch: (m) { 73 | spans.add( 74 | TextSpan( 75 | text: m.group(0), 76 | style: hyperLinkStyle, 77 | recognizer: TapGestureRecognizer() 78 | ..onTap = 79 | () => _launchUrl("mailto:${m.group(0)}?subject= &body= "), 80 | ), 81 | ); 82 | 83 | return ""; 84 | }, 85 | onNonMatch: (t) { 86 | t.splitMapJoin(hyperlinkRegex, onMatch: (m) { 87 | spans.add( 88 | TextSpan( 89 | text: m.group(0), 90 | style: hyperLinkStyle, 91 | recognizer: TapGestureRecognizer() 92 | ..onTap = () => _launchUrl( 93 | m.group(0)!, 94 | ), 95 | ), 96 | ); 97 | return ""; 98 | }, onNonMatch: (s) { 99 | spans.add( 100 | TextSpan(text: s), 101 | ); 102 | return ""; 103 | }); 104 | 105 | return ""; 106 | }, 107 | ); 108 | 109 | return ""; 110 | }, 111 | ); 112 | if (emojiString.isNotEmpty) { 113 | spans.add( 114 | TextSpan( 115 | text: emojiString, 116 | style: emojiStyle, 117 | ), 118 | ); 119 | emojiString = ""; 120 | } 121 | return spans; 122 | } 123 | 124 | void _launchUrl(String link) async { 125 | await launchUrl(link); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /lib/screens/startup/startup.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:vartalap/config/config_store.dart'; 4 | import 'package:vartalap/screens/chats/chats.dart'; 5 | import 'package:vartalap/services/chat_service.dart'; 6 | import 'package:vartalap/services/user_service.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:permission_handler/permission_handler.dart'; 9 | import 'package:vartalap/theme/theme.dart'; 10 | import 'package:vartalap/widgets/Inherited/config_provider.dart'; 11 | import 'package:vartalap/widgets/app_logo.dart'; 12 | 13 | class StartupScreen extends StatelessWidget { 14 | const StartupScreen({Key? key}) : super(key: key); 15 | 16 | Future _initializeApp( 17 | ConfigStore configStore, BuildContext context) async { 18 | unawaited(ChatService.init()); 19 | await Permission.notification.request(); 20 | var value = await Permission.contacts.status; 21 | if (value.isGranted) { 22 | onContactPermissionGranted(context); 23 | } 24 | onNext(context); 25 | } 26 | 27 | void onNext(BuildContext context) { 28 | Navigator.of(context).pushAndRemoveUntil( 29 | MaterialPageRoute( 30 | builder: (context) => Chats(), 31 | ), 32 | (route) => false, 33 | ); 34 | } 35 | 36 | void onContactPermissionGranted(BuildContext context) { 37 | unawaited(UserService.syncContacts(onInit: true)); 38 | } 39 | 40 | @override 41 | Widget build(BuildContext context) { 42 | final theme = Theme.of(context); 43 | final configStore = ConfigProvider.of(context).configStore; 44 | this._initializeApp(configStore, context); 45 | final packageInfo = configStore.packageInfo; 46 | 47 | return Scaffold( 48 | body: Stack( 49 | fit: StackFit.expand, 50 | children: [ 51 | Container( 52 | decoration: BoxDecoration(color: theme.primaryColor), 53 | ), 54 | Column( 55 | mainAxisAlignment: MainAxisAlignment.start, 56 | children: [ 57 | Expanded( 58 | flex: 3, 59 | child: Container( 60 | child: Column( 61 | mainAxisAlignment: MainAxisAlignment.center, 62 | children: [ 63 | AppLogo(size: 50), 64 | Padding( 65 | padding: EdgeInsets.only(top: 10.0), 66 | ), 67 | Text( 68 | packageInfo.appName, 69 | style: VartalapTheme.theme.appTitleStyle.copyWith( 70 | fontSize: 30, 71 | fontWeight: FontWeight.bold, 72 | ), 73 | ) 74 | ], 75 | ), 76 | ), 77 | ), 78 | Expanded( 79 | flex: 1, 80 | child: Column( 81 | mainAxisAlignment: MainAxisAlignment.center, 82 | children: [ 83 | CircularProgressIndicator( 84 | backgroundColor: Colors.white, 85 | color: theme.iconTheme.color, 86 | ), 87 | Padding( 88 | padding: EdgeInsets.only(top: 20.0), 89 | ), 90 | Text( 91 | configStore.subtitle, 92 | style: TextStyle( 93 | fontSize: 18.0, 94 | fontWeight: FontWeight.bold, 95 | color: Colors.white), 96 | ), 97 | Text( 98 | "v${packageInfo.version}+${packageInfo.buildNumber}", 99 | style: TextStyle(color: Colors.white), 100 | ) 101 | ], 102 | ), 103 | ) 104 | ], 105 | ) 106 | ], 107 | ), 108 | ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /lib/theme/theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:vartalap/utils/color_helper.dart'; 4 | 5 | final Color _primaryLightColor = Colors.blue; 6 | final Color _primaryDarkColor = Color.fromRGBO(0, 153, 122, 1); 7 | final Color _darkBackgroundColor = hexToColor("#222831"); 8 | final _defaultLightTheme = ThemeData.light(); 9 | final ThemeData _lightTheme = _defaultLightTheme.copyWith( 10 | appBarTheme: AppBarTheme( 11 | backgroundColor: Colors.blue, 12 | ), 13 | colorScheme: ColorScheme.fromSwatch( 14 | primarySwatch: Colors.blue, 15 | backgroundColor: Colors.grey, 16 | ), 17 | scaffoldBackgroundColor: Colors.grey[100], 18 | primaryColor: Colors.blue, 19 | visualDensity: VisualDensity.comfortable, 20 | iconTheme: _defaultLightTheme.iconTheme.copyWith(color: Colors.blue), 21 | highlightColor: Colors.blue, 22 | primaryColorLight: Colors.white, 23 | ); 24 | 25 | final _defaultDarkTheme = ThemeData.dark(); 26 | final ThemeData _darkTheme = _defaultDarkTheme.copyWith( 27 | appBarTheme: AppBarTheme( 28 | backgroundColor: hexToColor("#2C394B"), 29 | ), 30 | colorScheme: ColorScheme.fromSwatch( 31 | primarySwatch: generateMaterialColor(_primaryDarkColor), 32 | brightness: Brightness.dark, 33 | backgroundColor: _darkBackgroundColor, 34 | ), 35 | visualDensity: VisualDensity.comfortable, 36 | scaffoldBackgroundColor: _darkBackgroundColor, 37 | primaryColorLight: hexToColor("#2C394B"), 38 | iconTheme: _defaultDarkTheme.iconTheme.copyWith( 39 | color: _primaryDarkColor, 40 | ), 41 | cardColor: _darkBackgroundColor, 42 | ); 43 | 44 | final _appTitle = TextStyle( 45 | fontFamily: "sofia", 46 | ); 47 | 48 | @immutable 49 | class VartalapTheme { 50 | const VartalapTheme({ 51 | Key? key, 52 | required this.appTheme, 53 | required this.appTitleStyle, 54 | required this.appLogoColor, 55 | required this.linkTitleStyle, 56 | required this.receiverColor, 57 | required this.senderColor, 58 | this.readMessage = Colors.blueAccent, 59 | this.selectedRowColor = Colors.blueAccent, 60 | }); 61 | 62 | final TextStyle appTitleStyle; 63 | final Color appLogoColor; 64 | final TextStyle linkTitleStyle; 65 | final ThemeData appTheme; 66 | final Color senderColor; 67 | final Color receiverColor; 68 | final Color readMessage; 69 | final Color selectedRowColor; 70 | 71 | static ThemeMode get themeMode { 72 | ThemeMode t = kReleaseMode ? ThemeMode.system : ThemeMode.dark; 73 | return t; 74 | } 75 | 76 | static VartalapTheme get darkTheme { 77 | return VartalapTheme( 78 | appTheme: _darkTheme, 79 | appTitleStyle: _appTitle.copyWith( 80 | color: _primaryDarkColor, 81 | ), 82 | appLogoColor: _primaryDarkColor, 83 | linkTitleStyle: TextStyle( 84 | color: Colors.lightBlueAccent[100], 85 | decoration: TextDecoration.underline, 86 | ), 87 | receiverColor: _darkTheme.primaryColorLight, 88 | senderColor: _primaryDarkColor, 89 | readMessage: _darkBackgroundColor, 90 | selectedRowColor: _primaryDarkColor, 91 | ); 92 | } 93 | 94 | static VartalapTheme get lightTheme { 95 | return VartalapTheme( 96 | appTheme: _lightTheme, 97 | appTitleStyle: _appTitle.copyWith( 98 | color: _primaryLightColor, 99 | ), 100 | appLogoColor: Colors.blue, 101 | linkTitleStyle: TextStyle( 102 | color: Colors.purpleAccent[700], 103 | decoration: TextDecoration.underline, 104 | ), 105 | receiverColor: _lightTheme.primaryColorLight, 106 | senderColor: Colors.blue[300]!, 107 | readMessage: Colors.blue[900]!, 108 | ); 109 | } 110 | 111 | static VartalapTheme get theme { 112 | var themeMode = VartalapTheme.themeMode; 113 | switch (themeMode) { 114 | case ThemeMode.dark: 115 | return VartalapTheme.darkTheme; 116 | case ThemeMode.light: 117 | return VartalapTheme.lightTheme; 118 | case ThemeMode.system: 119 | final brightness = MediaQueryData.fromView( 120 | WidgetsBinding.instance.platformDispatcher.views.single) 121 | .platformBrightness; 122 | return brightness == Brightness.dark 123 | ? VartalapTheme.darkTheme 124 | : VartalapTheme.lightTheme; 125 | default: 126 | return VartalapTheme.lightTheme; 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /lib/models/remoteMessage.dart: -------------------------------------------------------------------------------- 1 | import 'package:vartalap/models/chat.dart'; 2 | import 'package:vartalap/models/message.dart'; 3 | import 'package:vartalap/utils/enum_helper.dart'; 4 | 5 | class Head { 6 | Head({ 7 | required this.type, 8 | required this.to, 9 | required this.from, 10 | required this.chatid, 11 | required this.contentType, 12 | required this.action, 13 | this.category = 'message', 14 | this.ephemeral = false, 15 | }); 16 | 17 | late ChatType type; 18 | late String to; 19 | late String from; 20 | late String? chatid; 21 | late MessageType contentType; 22 | late String action; 23 | late String category; 24 | late bool ephemeral; 25 | 26 | Head.fromMap(Map map) { 27 | this.type = stringToEnum(map["type"], ChatType.values); 28 | this.to = map["to"]; 29 | this.from = map["from"]; 30 | this.action = map["action"]; 31 | this.contentType = stringToEnum(map["contentType"], MessageType.values); 32 | this.chatid = map["chatid"]; 33 | this.category = map.containsKey("category") ? map["category"] : 'message'; 34 | this.ephemeral = map.containsKey("ephemeral") ? map["ephemeral"] : false; 35 | } 36 | 37 | Map toMap() { 38 | return { 39 | "to": this.to, 40 | "from": this.from, 41 | "action": this.action, 42 | "chatid": this.chatid, 43 | "type": enumToString(this.type), 44 | "contentType": enumToString(this.contentType), 45 | "category": this.category, 46 | "ephemeral": this.ephemeral, 47 | }; 48 | } 49 | } 50 | 51 | class Meta { 52 | Map raw = {}; 53 | String get hash => raw['hash']; 54 | String get contentHash => raw['contentHash']; 55 | int get createdAt => raw.containsKey('createdAt') 56 | ? raw['createdAt'] 57 | : DateTime.now().millisecondsSinceEpoch; 58 | 59 | Meta({String? hash, String? contentHash, int? createdAt}) { 60 | if (hash != null) { 61 | this.raw['hash'] = hash; 62 | } 63 | if (contentHash != null) { 64 | this.raw['contentHash'] = contentHash; 65 | } 66 | this.raw['createdAt'] = 67 | createdAt == null ? DateTime.now().millisecondsSinceEpoch : createdAt; 68 | } 69 | 70 | Meta.fromMap(Map map) { 71 | this.raw = map; 72 | } 73 | 74 | Map toMap() { 75 | return this.raw; 76 | } 77 | } 78 | 79 | class RemoteMessage { 80 | double _v = 2.1; 81 | double get ver => _v; 82 | late String id; 83 | late Head head; 84 | late Meta meta; 85 | late Map body; 86 | 87 | RemoteMessage.fromMessage(ChatMessage stateMsg, String to, ChatType type) { 88 | this.id = stateMsg.id; 89 | this.head = Head( 90 | action: stateMsg.action, 91 | contentType: stateMsg.type, 92 | chatid: stateMsg.chatId, 93 | from: stateMsg.senderId, 94 | type: type, 95 | to: to, 96 | category: stateMsg.category, 97 | ephemeral: stateMsg.ephemeral, 98 | ); 99 | 100 | this.meta = Meta( 101 | createdAt: stateMsg.timestamp, 102 | contentHash: stateMsg.calcContentHash(), 103 | ); 104 | 105 | this.body = stateMsg.toRemoteBody(); 106 | } 107 | 108 | RemoteMessage.fromMap(Map map, {bool ignoreError = false}) { 109 | if (!map.containsKey("_v") && ignoreError) { 110 | // If the message doesn't counts version 111 | // It's probably an older message stuck in pending queue 112 | // If Ignore Error flag is set, Simply create a dummy Remote message 113 | this._v = 1.0; 114 | this.id = map.containsKey("id") ? map["id"] : ""; 115 | this.head = Head( 116 | type: ChatType.OTHER, 117 | to: "", 118 | from: "", 119 | chatid: "", 120 | contentType: MessageType.OTHER, 121 | action: "", 122 | ); 123 | this.meta = Meta(); 124 | this.body = {}; 125 | return; 126 | } 127 | this._v = map["_v"] + .0; 128 | this.id = map["id"]; 129 | this.head = Head.fromMap(map["head"]); 130 | this.meta = map.containsKey("meta") ? Meta.fromMap(map["meta"]) : Meta(); 131 | this.body = map["body"]; 132 | } 133 | 134 | Map toMap() { 135 | Map map = { 136 | "_v": this.ver, 137 | "id": this.id, 138 | "head": this.head.toMap(), 139 | "meta": this.meta.toMap(), 140 | "body": this.body 141 | }; 142 | return map; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /lib/widgets/chat_preview.dart: -------------------------------------------------------------------------------- 1 | import 'package:vartalap/models/chat.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:vartalap/theme/theme.dart'; 4 | import 'package:vartalap/utils/dateTimeFormat.dart'; 5 | import 'package:vartalap/widgets/avator.dart'; 6 | 7 | class ChatPreviewWidget extends StatelessWidget { 8 | final ChatPreview _chat; 9 | final Function _onTap; 10 | final Function _onLongPress; 11 | final bool isSelected; 12 | ChatPreviewWidget(this._chat, this._onTap, this._onLongPress, 13 | {this.isSelected = false}) 14 | : super(key: Key(_chat.id)); 15 | @override 16 | Widget build(BuildContext context) { 17 | final vtheme = VartalapTheme.theme; 18 | return new Column( 19 | children: [ 20 | ListTileTheme( 21 | selectedColor: vtheme.selectedRowColor, 22 | child: ListTile( 23 | leading: Container( 24 | width: 42, 25 | height: 42, 26 | child: Stack( 27 | children: [ 28 | Avator( 29 | width: 42.0, 30 | height: 42.0, 31 | text: this._chat.title, 32 | ), 33 | this.isSelected 34 | ? Positioned( 35 | bottom: 0, 36 | right: 0, 37 | child: CircleAvatar( 38 | backgroundColor: vtheme.selectedRowColor, 39 | radius: 10, 40 | child: Icon( 41 | Icons.check, 42 | color: Colors.white, 43 | size: 14, 44 | ), 45 | ), 46 | ) 47 | : Container() 48 | ], 49 | ), 50 | ), 51 | // leading: new ProfileImg( 52 | // this._chat.pic ?? 'assets/images/default-user.png', 53 | // ProfileImgSize.MD), 54 | title: new Row( 55 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 56 | children: [ 57 | new Text( 58 | this._chat.title, 59 | style: new TextStyle( 60 | fontWeight: FontWeight.bold, 61 | ), 62 | ), 63 | new Text( 64 | (this._chat.ts) != 0 65 | ? formatMessageTimestamp(this._chat.ts) 66 | : '', 67 | style: new TextStyle(fontSize: 12.0), 68 | ), 69 | ], 70 | ), 71 | subtitle: new Row( 72 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 73 | children: [ 74 | Expanded( 75 | child: Text( 76 | this._chat.content, 77 | softWrap: false, 78 | overflow: TextOverflow.ellipsis, 79 | style: new TextStyle(fontSize: 15.0), 80 | ), 81 | ), 82 | getWidget(context) 83 | ], 84 | ), 85 | onTap: () => this._onTap(this._chat), 86 | onLongPress: () => this._onLongPress(this._chat), 87 | selected: this.isSelected, 88 | ), 89 | ), 90 | new Divider( 91 | height: 5.0, 92 | ), 93 | ], 94 | ); 95 | } 96 | 97 | Widget getWidget(BuildContext context) { 98 | final vtheme = VartalapTheme.theme; 99 | return this._chat.unread > 0 100 | ? Container( 101 | width: 24, 102 | height: 24, 103 | decoration: BoxDecoration( 104 | shape: BoxShape.circle, 105 | color: vtheme.selectedRowColor, 106 | ), 107 | child: Center( 108 | child: Text( 109 | _getUnreadCountText(), 110 | textAlign: TextAlign.center, 111 | style: TextStyle( 112 | fontWeight: FontWeight.bold, 113 | fontSize: 11, 114 | color: Colors.white, 115 | ), 116 | )), 117 | ) 118 | : Text(""); 119 | } 120 | 121 | String _getUnreadCountText() { 122 | if (this._chat.unread > 9) { 123 | return "9+"; 124 | } 125 | return this._chat.unread.toString(); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:firebase_app_check/firebase_app_check.dart'; 4 | import 'package:firebase_core/firebase_core.dart'; 5 | import 'package:firebase_crashlytics/firebase_crashlytics.dart'; 6 | import 'package:flutter/foundation.dart'; 7 | import 'package:vartalap/config/config_store.dart'; 8 | import 'package:vartalap/screens/login/introduction.dart'; 9 | import 'package:vartalap/screens/new_chat/create_group.dart'; 10 | import 'package:vartalap/screens/new_chat/select_group_member.dart'; 11 | import 'package:vartalap/screens/startup/startup.dart'; 12 | import 'package:flutter/material.dart'; 13 | import 'package:vartalap/services/auth_service.dart'; 14 | import 'package:vartalap/services/chat_service.dart'; 15 | import 'package:vartalap/services/performance_metric.dart'; 16 | import 'package:vartalap/theme/theme.dart'; 17 | import 'package:vartalap/widgets/Inherited/auth_listener.dart'; 18 | import 'package:vartalap/widgets/Inherited/config_provider.dart'; 19 | import 'package:vartalap/models/chat.dart'; 20 | import 'package:vartalap/models/user.dart'; 21 | import 'package:vartalap/screens/chats/chats.dart'; 22 | import 'package:vartalap/screens/chat/chat.dart'; 23 | import 'package:vartalap/screens/new_chat/new_chat.dart'; 24 | import 'package:vartalap/services/crashlystics.dart'; 25 | 26 | final configStore = ConfigStore(); 27 | void main() async { 28 | WidgetsFlutterBinding.ensureInitialized(); 29 | final homescreen = await initializeApp(); 30 | FlutterError.onError = (errorDetails) { 31 | FirebaseCrashlytics.instance.recordFlutterFatalError(errorDetails); 32 | }; 33 | // Pass all uncaught asynchronous errors that aren't handled by the Flutter framework to Crashlytics 34 | PlatformDispatcher.instance.onError = (error, stack) { 35 | FirebaseCrashlytics.instance.recordError(error, stack, fatal: true); 36 | return true; 37 | }; 38 | runApp(Home(configStore.packageInfo.appName, homescreen)); 39 | } 40 | 41 | Future initializeApp() async { 42 | await Firebase.initializeApp(); 43 | await FirebaseAppCheck.instance.activate( 44 | webRecaptchaSiteKey: 'recaptcha-v3-site-key', 45 | androidProvider: AndroidProvider.playIntegrity, 46 | appleProvider: AppleProvider.appAttestWithDeviceCheckFallback, 47 | ); 48 | await configStore.loadConfig(); 49 | await AuthService.init(); 50 | Crashlytics.init(); 51 | PerformanceMetric.init(); 52 | if (AuthService.instance.isLoggedIn()) { 53 | return StartupScreen(); 54 | } 55 | return IntroductionScreen(); 56 | } 57 | 58 | class Home extends StatefulWidget { 59 | final String appName; 60 | final Widget homeScreen; 61 | Home(this.appName, this.homeScreen); 62 | @override 63 | HomeState createState() => HomeState(); 64 | } 65 | 66 | class HomeState extends State { 67 | final _navigatorKey = GlobalKey(); 68 | @override 69 | initState() { 70 | super.initState(); 71 | } 72 | 73 | // This widget is the root of your application. 74 | @override 75 | Widget build(BuildContext context) { 76 | return ConfigProvider( 77 | configStore: configStore, 78 | child: AuthListner( 79 | app: MaterialApp( 80 | title: widget.appName, 81 | debugShowCheckedModeBanner: false, 82 | navigatorKey: _navigatorKey, 83 | themeMode: VartalapTheme.themeMode, 84 | theme: VartalapTheme.lightTheme.appTheme, 85 | darkTheme: VartalapTheme.darkTheme.appTheme, 86 | onGenerateRoute: _routes(), 87 | home: widget.homeScreen, 88 | ), 89 | ), 90 | ); 91 | } 92 | 93 | RouteFactory _routes() { 94 | return (RouteSettings settings) { 95 | Widget widget; 96 | switch (settings.name) { 97 | case '/': 98 | widget = new StartupScreen(); 99 | break; 100 | case '/chats': 101 | widget = new Chats(); 102 | break; 103 | case '/chat': 104 | widget = new ChatScreen(settings.arguments as Chat); 105 | break; 106 | case '/new-chat': 107 | widget = new NewChatScreen(); 108 | break; 109 | case '/new-group': 110 | widget = new SelectGroupMemberScreen(); 111 | break; 112 | case '/create-group': 113 | widget = new CreateGroup(settings.arguments as List); 114 | break; 115 | default: 116 | widget = new Chats(); 117 | } 118 | return new MaterialPageRoute(builder: (BuildContext context) => widget); 119 | }; 120 | } 121 | 122 | @override 123 | void dispose() { 124 | ChatService.dispose(); 125 | AuthService.instance.dispose(); 126 | super.dispose(); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /lib/utils/chat_message_helper.dart: -------------------------------------------------------------------------------- 1 | import 'package:vartalap/models/dateHeader.dart'; 2 | import 'package:vartalap/models/message.dart'; 3 | import 'package:vartalap/models/messageSpacer.dart'; 4 | import 'package:vartalap/models/previewImage.dart'; 5 | import 'package:vartalap/models/remoteMessage.dart'; 6 | import 'package:vartalap/models/user.dart'; 7 | import 'package:vartalap/utils/dateTimeFormat.dart'; 8 | import 'package:vartalap/utils/enum_helper.dart'; 9 | 10 | List calculateChatMessages( 11 | List messages, 12 | User user, { 13 | String Function(DateTime)? customDateHeaderText, 14 | required bool showUserNames, 15 | }) { 16 | final chatMessages = []; 17 | final gallery = []; 18 | 19 | var shouldShowName = false; 20 | 21 | for (var i = messages.length - 1; i >= 0; i--) { 22 | final isFirst = i == messages.length - 1; 23 | final isLast = i == 0; 24 | final message = messages[i]; 25 | final nextMessage = isLast ? null : messages[i - 1]; 26 | final nextMessageHasCreatedAt = nextMessage?.timestamp != null; 27 | final nextMessageSameAuthor = message.senderId == nextMessage?.senderId; 28 | final notMyMessage = message.senderId != user.username; 29 | 30 | var nextMessageDateThreshold = false; 31 | var nextMessageDifferentDay = false; 32 | var nextMessageInGroup = false; 33 | var showName = false; 34 | 35 | if (showUserNames) { 36 | final previousMessage = isFirst ? null : messages[i + 1]; 37 | 38 | final isFirstInGroup = notMyMessage && 39 | ((message.senderId != previousMessage?.senderId) || 40 | (message.timestamp - previousMessage!.timestamp > 60000)); 41 | 42 | if (isFirstInGroup) { 43 | shouldShowName = false; 44 | if (message.type == MessageType.TEXT) { 45 | showName = true; 46 | } else { 47 | shouldShowName = true; 48 | } 49 | } 50 | 51 | if (message.type == MessageType.TEXT && shouldShowName) { 52 | showName = true; 53 | shouldShowName = false; 54 | } 55 | } 56 | 57 | if (nextMessageHasCreatedAt) { 58 | nextMessageDifferentDay = 59 | DateTime.fromMillisecondsSinceEpoch(message.timestamp).day != 60 | DateTime.fromMillisecondsSinceEpoch(nextMessage!.timestamp).day; 61 | 62 | nextMessageInGroup = nextMessageSameAuthor && 63 | nextMessage.timestamp - message.timestamp <= 60000; 64 | } 65 | 66 | if (isFirst) { 67 | chatMessages.insert( 68 | 0, 69 | DateHeader( 70 | date: formatMessageDate(message.timestamp), 71 | ), 72 | ); 73 | } 74 | 75 | chatMessages.insert(0, { 76 | 'message': message, 77 | 'nextMessageInGroup': nextMessageInGroup, 78 | 'showName': notMyMessage && showUserNames && showName, 79 | 'showStatus': true, 80 | 'showNip': !nextMessageInGroup, 81 | }); 82 | 83 | if (!nextMessageInGroup) { 84 | chatMessages.insert( 85 | 0, 86 | MessageSpacer( 87 | height: 5, 88 | id: message.id, 89 | ), 90 | ); 91 | } 92 | 93 | if (nextMessageDifferentDay || nextMessageDateThreshold) { 94 | chatMessages.insert( 95 | 0, 96 | DateHeader( 97 | date: formatMessageDate(nextMessage!.timestamp), 98 | ), 99 | ); 100 | } 101 | 102 | // if (message.type == MessageType.IMAGE) { 103 | // gallery.add(PreviewImage(id: message.id, uri: message.uri)); 104 | // } 105 | } 106 | 107 | return [chatMessages, gallery]; 108 | } 109 | 110 | ChatMessage toChatMessage(RemoteMessage msg) { 111 | ChatMessage chatMsg; 112 | if (msg.head.contentType == MessageType.NOTIFICATION && 113 | msg.head.action == "state") { 114 | chatMsg = StateMessge( 115 | msg.head.chatid!, 116 | msg.head.from, 117 | MessageState.OTHER, 118 | ); 119 | } else if (msg.head.contentType == MessageType.NOTIFICATION && 120 | msg.head.action == "typing") { 121 | chatMsg = TypingMessage(msg.head.chatid!, msg.head.from, false); 122 | } else if (msg.head.contentType == MessageType.TEXT) { 123 | chatMsg = TextMessage( 124 | msg.id, 125 | msg.head.chatid!, 126 | msg.head.from, 127 | ); 128 | } else { 129 | chatMsg = CustomMessage( 130 | msg.id, 131 | msg.head.chatid!, 132 | msg.head.from, 133 | ); 134 | } 135 | 136 | chatMsg.fromRemoteBody(msg.body); 137 | return chatMsg; 138 | } 139 | 140 | ChatMessage buildChatMessage(Map map, 141 | {bool persistent = false}) { 142 | final type = intToEnum(map["type"], MessageType.values); 143 | switch (type) { 144 | case MessageType.TEXT: 145 | return TextMessage.fromMap(map, persistent: persistent); 146 | default: 147 | return CustomMessage.fromMap(map, persistent: persistent); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /lib/services/auth_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:firebase_auth/firebase_auth.dart'; 4 | import 'package:flutter_secure_storage/flutter_secure_storage.dart'; 5 | import 'package:vartalap/services/api_service.dart'; 6 | import 'package:vartalap/services/crashlystics.dart'; 7 | 8 | class AuthResponse { 9 | late String phoneNumber; 10 | late String token; 11 | late bool status; 12 | late dynamic error; 13 | } 14 | 15 | class AuthService { 16 | FirebaseAuth _auth = FirebaseAuth.instance; 17 | String? _phoneNumber; 18 | int? _resendToken; 19 | late String _verificationId; 20 | User? _user; 21 | static FlutterSecureStorage _storage = new FlutterSecureStorage(); 22 | static AuthService? _instance; 23 | 24 | StreamController authStateController = 25 | StreamController.broadcast(); 26 | Stream get authStateChange => authStateController.stream; 27 | AuthService() { 28 | _auth.authStateChanges().listen((event) { 29 | _user = event; 30 | }); 31 | } 32 | 33 | Future sendOtp(String phonenumber) async { 34 | Completer _promise = Completer(); 35 | if (phonenumber != _phoneNumber) { 36 | _resendToken = null; 37 | try { 38 | await _storage.deleteAll(); 39 | } catch (e, stack) { 40 | Crashlytics.recordError(e, stack, 41 | reason: "Error while access secure storage"); 42 | } 43 | } 44 | _phoneNumber = phonenumber; 45 | _auth.verifyPhoneNumber( 46 | timeout: Duration(seconds: 0), 47 | phoneNumber: _phoneNumber!, 48 | forceResendingToken: _resendToken, 49 | codeSent: (String verificationId, int? resendToken) async { 50 | _resendToken = resendToken; 51 | _verificationId = verificationId; 52 | try { 53 | await _storage.write( 54 | key: 'resendToken', value: resendToken.toString()); 55 | await _storage.write(key: 'phoneNumber', value: _phoneNumber); 56 | } catch (e, stack) { 57 | Crashlytics.recordError(e, stack, 58 | reason: "Error while access secure storage"); 59 | } 60 | 61 | _promise.complete(true); 62 | }, 63 | codeAutoRetrievalTimeout: (verificationId) {}, 64 | verificationCompleted: (phoneAuthCredential) {}, 65 | verificationFailed: (error) { 66 | _promise.complete(false); 67 | }, 68 | ); 69 | return _promise.future; 70 | } 71 | 72 | Future reSendOtp() { 73 | return sendOtp(_phoneNumber!); 74 | } 75 | 76 | Future verify(String otp) async { 77 | PhoneAuthCredential credential = PhoneAuthProvider.credential( 78 | verificationId: _verificationId, smsCode: otp); 79 | AuthResponse _resp = AuthResponse(); 80 | try { 81 | var result = await _auth.signInWithCredential(credential); 82 | _resp.phoneNumber = _phoneNumber!; 83 | _user = result.user; 84 | var idTokenResult = await result.user!.getIdTokenResult(); 85 | _resp.token = idTokenResult.token!; 86 | _resp.status = true; 87 | } catch (e, stack) { 88 | _resp.error = e; 89 | _resp.status = false; 90 | _resp.phoneNumber = _phoneNumber!; 91 | Crashlytics.recordError(e, stack, 92 | reason: "Error while authentication with firebase"); 93 | } 94 | if (_resp.status) { 95 | try { 96 | await ApiService.login(_phoneNumber!); 97 | this.authStateController.sink.add(true); 98 | } catch (e, stack) { 99 | Crashlytics.recordError(e, stack, reason: "Login api service failed"); 100 | await _auth.signOut(); 101 | _resp.error = e; 102 | _resp.status = false; 103 | } 104 | } 105 | 106 | return _resp; 107 | } 108 | 109 | bool isLoggedIn() { 110 | return _user != null; 111 | } 112 | 113 | Future signout() async { 114 | await this._auth.signOut(); 115 | authStateController.sink.add(false); 116 | } 117 | 118 | String? get phoneNumber { 119 | if (isLoggedIn()) { 120 | return _user!.phoneNumber; 121 | } 122 | return null; 123 | } 124 | 125 | Future get idToken { 126 | if (isLoggedIn()) { 127 | return _user!.getIdToken(); 128 | } 129 | return Future.value(null); 130 | } 131 | 132 | dispose() { 133 | this.authStateController.close(); 134 | } 135 | 136 | static AuthService get instance { 137 | if (_instance == null) { 138 | _instance = AuthService(); 139 | } 140 | return _instance!; 141 | } 142 | 143 | static Future init() async { 144 | try { 145 | String? _phoneNumber = await _storage.read(key: 'phoneNumber'); 146 | if (_phoneNumber != null) { 147 | instance._phoneNumber = _phoneNumber; 148 | } 149 | String? _resendToken = await _storage.read(key: 'resendToken'); 150 | if (_resendToken != null) { 151 | instance._resendToken = int.parse(_resendToken); 152 | } 153 | instance._user = _instance!._auth.currentUser; 154 | } catch (e, stack) { 155 | Crashlytics.recordError(e, stack, 156 | reason: "Error while initializing auth service"); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | report@one9x.org. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. -------------------------------------------------------------------------------- /lib/widgets/message.dart: -------------------------------------------------------------------------------- 1 | import 'package:bubble/bubble.dart'; 2 | import 'package:vartalap/models/message.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:vartalap/theme/theme.dart'; 5 | import 'package:vartalap/utils/color_helper.dart'; 6 | import 'package:vartalap/utils/dateTimeFormat.dart'; 7 | import 'package:vartalap/widgets/rich_message.dart'; 8 | 9 | class MessageWidget extends StatelessWidget { 10 | final ChatMessage _msg; 11 | final bool _isYou; 12 | 13 | final bool isSelected; 14 | final Function? onTab; 15 | final Function? onLongPress; 16 | final bool showUserInfo; 17 | final bool showNip; 18 | MessageWidget( 19 | this._msg, 20 | this._isYou, { 21 | Key? key, 22 | this.isSelected = false, 23 | this.onTab, 24 | this.onLongPress, 25 | this.showUserInfo = false, 26 | this.showNip = true, 27 | }) : super(key: Key(_msg.id)); 28 | 29 | @override 30 | Widget build(BuildContext context) { 31 | final senderColor = VartalapTheme.theme.senderColor; 32 | final receiverColor = VartalapTheme.theme.receiverColor; 33 | final selectedRowColor = VartalapTheme.theme.selectedRowColor; 34 | return GestureDetector( 35 | onTap: () { 36 | this.onTab!(this._msg); 37 | }, 38 | onLongPress: () { 39 | this.onLongPress!(this._msg); 40 | }, 41 | child: Container( 42 | padding: const EdgeInsets.only(bottom: 2), 43 | decoration: BoxDecoration( 44 | color: this.isSelected ? selectedRowColor : Colors.transparent, 45 | ), 46 | constraints: BoxConstraints( 47 | minWidth: double.infinity, 48 | ), 49 | child: Bubble( 50 | alignment: this._isYou ? Alignment.topRight : Alignment.topLeft, 51 | color: this.isSelected 52 | ? selectedRowColor 53 | : this._isYou 54 | ? senderColor 55 | : receiverColor, 56 | showNip: this.showNip, 57 | nip: this._isYou ? BubbleNip.rightBottom : BubbleNip.leftBottom, 58 | child: Container( 59 | constraints: BoxConstraints( 60 | maxWidth: MediaQuery.of(context).size.width * 0.75, 61 | ), 62 | child: Column( 63 | mainAxisSize: MainAxisSize.min, 64 | crossAxisAlignment: CrossAxisAlignment.start, 65 | textBaseline: TextBaseline.ideographic, 66 | children: getMessageComponents(context), 67 | ), 68 | ), 69 | ), 70 | ), 71 | ); 72 | } 73 | 74 | List getMessageComponents(BuildContext context) { 75 | List _widgets = []; 76 | if (this.showUserInfo) { 77 | final brightness = Theme.of(context).brightness; 78 | _widgets.add( 79 | Container( 80 | margin: EdgeInsets.only(bottom: 4), 81 | child: Text( 82 | this._msg.sender == null ? '' : this._msg.sender!.name, 83 | textAlign: TextAlign.start, 84 | style: TextStyle( 85 | fontSize: 12, 86 | color: getColor( 87 | this._msg.sender!.name, 88 | opacity: 1, 89 | brightness: brightness, 90 | ), 91 | ), 92 | ), 93 | ), 94 | ); 95 | } 96 | _widgets.add( 97 | Wrap( 98 | alignment: WrapAlignment.end, 99 | crossAxisAlignment: WrapCrossAlignment.end, 100 | children: [ 101 | Container( 102 | constraints: BoxConstraints( 103 | minWidth: MediaQuery.of(context).size.width * 0.25, 104 | ), 105 | child: getMessageWidget(context), 106 | ), 107 | Container( 108 | margin: EdgeInsets.only(left: 5), 109 | child: Row( 110 | mainAxisSize: MainAxisSize.min, 111 | mainAxisAlignment: MainAxisAlignment.end, 112 | crossAxisAlignment: CrossAxisAlignment.baseline, 113 | textBaseline: TextBaseline.ideographic, 114 | children: [ 115 | Text( 116 | formatMessageTime(this._msg.timestamp), 117 | style: TextStyle( 118 | fontSize: 11.0, 119 | ), 120 | ), 121 | SizedBox( 122 | width: 8.0, 123 | ), 124 | _isYou ? _getIcon() : Container() 125 | ], 126 | ), 127 | ), 128 | ], 129 | ), 130 | ); 131 | return _widgets; 132 | } 133 | 134 | Widget getMessageWidget(BuildContext context) { 135 | final theme = Theme.of(context); 136 | final TextStyle textStyle = TextStyle( 137 | fontSize: theme.primaryTextTheme.titleMedium!.fontSize, 138 | fontWeight: theme.primaryTextTheme.titleMedium!.fontWeight, 139 | letterSpacing: 0.25, 140 | color: theme.textTheme.bodyLarge?.color, 141 | ); 142 | switch (this._msg.type) { 143 | case MessageType.TEXT: 144 | { 145 | final msg = this._msg as TextMessage; 146 | return RichMessage( 147 | msg.text, 148 | textStyle, 149 | ); 150 | } 151 | default: 152 | return SizedBox(); 153 | } 154 | } 155 | 156 | Widget _getIcon() { 157 | IconData icon = Icons.access_time; 158 | Color color = Colors.white; 159 | switch (this._msg.state) { 160 | case MessageState.NEW: 161 | icon = Icons.access_time; 162 | break; 163 | case MessageState.SENT: 164 | icon = Icons.check; 165 | break; 166 | case MessageState.DELIVERED: 167 | icon = Icons.done_all; 168 | break; 169 | case MessageState.OTHER: 170 | return Container(); 171 | case MessageState.READ: 172 | icon = Icons.done_all_sharp; 173 | color = VartalapTheme.theme.readMessage; 174 | break; 175 | } 176 | 177 | return Icon( 178 | icon, 179 | size: 15.0, 180 | color: color, 181 | ); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /lib/services/push_notification_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'package:flutter/widgets.dart'; 4 | 5 | import 'package:firebase_core/firebase_core.dart'; 6 | import 'package:firebase_messaging/firebase_messaging.dart'; 7 | import 'package:flutter_local_notifications/flutter_local_notifications.dart'; 8 | import 'package:vartalap/config/config_store.dart'; 9 | import 'package:vartalap/models/message.dart'; 10 | import 'package:vartalap/models/remoteMessage.dart' as vRemoteMessage; 11 | import 'package:vartalap/services/auth_service.dart'; 12 | import 'package:vartalap/services/chat_service.dart'; 13 | import 'package:vartalap/utils/chat_message_helper.dart'; 14 | import 'package:vartalap/utils/remote_message_helper.dart'; 15 | 16 | Future showNotificationService(String title, String body, dynamic payload, 17 | {String? groupKey, int id = 0}) { 18 | const AndroidInitializationSettings initializationSettingsAndroid = 19 | AndroidInitializationSettings('@mipmap/ic_launcher'); 20 | var _initializationSettings = 21 | InitializationSettings(android: initializationSettingsAndroid); 22 | 23 | var _androidPlatformChannelSpecifics = AndroidNotificationDetails( 24 | 'VARTALAP_NOTIFICATION', 25 | 'VARTALAP_NOTIFICATION', 26 | channelDescription: 'Vartalap notification channel', 27 | importance: Importance.max, 28 | priority: Priority.high, 29 | ticker: 'Vartalap notification', 30 | showWhen: true, 31 | playSound: true, 32 | groupKey: groupKey, 33 | setAsGroupSummary: true, 34 | groupAlertBehavior: GroupAlertBehavior.summary, 35 | ); 36 | var _notificationDetails = 37 | NotificationDetails(android: _androidPlatformChannelSpecifics); 38 | var flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); 39 | flutterLocalNotificationsPlugin.initialize(_initializationSettings); 40 | var data = json.encode(payload); 41 | return flutterLocalNotificationsPlugin 42 | .show(data.hashCode, title, body, _notificationDetails, payload: data); 43 | } 44 | 45 | @pragma('vm:entry-point') 46 | Future fcmBackgroundMessageHandler(RemoteMessage payload) async { 47 | await Firebase.initializeApp(); 48 | await ConfigStore().loadConfig(); 49 | await AuthService.init(); 50 | final event = payload.data["message"]; 51 | final messages = toRemoteMessage(event); 52 | final List deliveryAcks = []; 53 | for (var msg in messages) { 54 | var result = await ChatService.newMessage(msg); 55 | if (result != null && msg.head.contentType != MessageType.NOTIFICATION) { 56 | var chat = await ChatService.getChatInfo(msg.head.chatid!); 57 | if (chat == null) return; 58 | deliveryAcks.add(result); 59 | final notify = toChatMessage(msg).notificationContent; 60 | if (notify.show && notify.content != null) { 61 | showNotificationService(chat.title, notify.content!, msg.toMap(), 62 | groupKey: chat.id, id: chat.id.hashCode); 63 | } 64 | } 65 | } 66 | return await ChatService.ackMessageDelivery(deliveryAcks, socket: false); 67 | } 68 | 69 | class PushNotificationService { 70 | static PushNotificationService? _instance; 71 | late InitializationSettings _initializationSettings; 72 | final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin = 73 | FlutterLocalNotificationsPlugin(); 74 | 75 | PushNotificationService() { 76 | const AndroidInitializationSettings initializationSettingsAndroid = 77 | AndroidInitializationSettings('@mipmap/ic_launcher'); 78 | _initializationSettings = 79 | InitializationSettings(android: initializationSettingsAndroid); 80 | 81 | this.clearAllNotification(); 82 | } 83 | 84 | void config({required Function onMessage}) async { 85 | FirebaseMessaging.onMessage.listen((event) { 86 | onMessage({"data": event.data}); 87 | }); 88 | _flutterLocalNotificationsPlugin.initialize( 89 | _initializationSettings, 90 | onDidReceiveNotificationResponse: (details) { 91 | if (details.payload != null) { 92 | var decoded = json.decode(details.payload!); 93 | Map data = Map.from(decoded); 94 | return onMessage({ 95 | "data": {"message": data}, 96 | "source": "ON_NOTIFICATION_TAP" 97 | }); 98 | } 99 | }, 100 | onDidReceiveBackgroundNotificationResponse: (details) { 101 | if (details.payload != null) { 102 | var decoded = json.decode(details.payload!); 103 | Map data = Map.from(decoded); 104 | return onMessage({ 105 | "data": {"message": data}, 106 | "source": "ON_NOTIFICATION_TAP" 107 | }); 108 | } 109 | }, 110 | ); 111 | } 112 | 113 | Future get token => FirebaseMessaging.instance.getToken(); 114 | 115 | void showNotification(String title, String body, dynamic payload, 116 | {String? groupKey, int id = 0}) { 117 | var _androidPlatformChannelSpecifics = AndroidNotificationDetails( 118 | 'VARTALAP_NOTIFICATION', 119 | 'VARTALAP_NOTIFICATION', 120 | channelDescription: 'Vartalap notification channel', 121 | importance: Importance.max, 122 | priority: Priority.high, 123 | ticker: 'Vartalap notification', 124 | showWhen: true, 125 | playSound: true, 126 | timeoutAfter: 127 | WidgetsBinding.instance.lifecycleState == AppLifecycleState.resumed 128 | ? 500 129 | : null, 130 | groupKey: groupKey, 131 | setAsGroupSummary: true, 132 | groupAlertBehavior: GroupAlertBehavior.all, 133 | ); 134 | 135 | var _notificationDetails = 136 | NotificationDetails(android: _androidPlatformChannelSpecifics); 137 | var data = json.encode(payload); 138 | 139 | _flutterLocalNotificationsPlugin.show(id, title, body, _notificationDetails, 140 | payload: data); 141 | } 142 | 143 | void clearAllNotification() { 144 | _flutterLocalNotificationsPlugin.cancelAll(); 145 | } 146 | 147 | static PushNotificationService get instance { 148 | if (_instance == null) { 149 | FirebaseMessaging.onBackgroundMessage(fcmBackgroundMessageHandler); 150 | _instance = PushNotificationService(); 151 | } 152 | return _instance!; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /lib/screens/login/introduction.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/gestures.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:vartalap/config/config_store.dart'; 4 | import 'package:vartalap/theme/theme.dart'; 5 | import 'package:vartalap/utils/url_helper.dart'; 6 | import 'package:vartalap/widgets/app_logo.dart'; 7 | 8 | import 'package:vartalap/screens/login/login.dart'; 9 | 10 | class IntroductionScreen extends StatelessWidget { 11 | final config = ConfigStore(); 12 | @override 13 | Widget build(BuildContext context) { 14 | final theme = VartalapTheme.theme; 15 | final linkTheme = theme.linkTitleStyle.copyWith( 16 | fontWeight: FontWeight.bold, 17 | ); 18 | return Scaffold( 19 | body: Padding( 20 | padding: const EdgeInsets.all(10.0), 21 | child: Column( 22 | children: [ 23 | Expanded( 24 | flex: 2, 25 | child: Column( 26 | mainAxisAlignment: MainAxisAlignment.center, 27 | crossAxisAlignment: CrossAxisAlignment.center, 28 | children: [ 29 | Container( 30 | child: Center( 31 | child: Container( 32 | constraints: const BoxConstraints(maxHeight: 340), 33 | margin: const EdgeInsets.symmetric(horizontal: 8), 34 | child: AppLogo( 35 | size: 45, 36 | ), 37 | ), 38 | ), 39 | ), 40 | Container( 41 | margin: EdgeInsets.only(top: 10), 42 | child: Text( 43 | config.packageInfo.appName, 44 | style: VartalapTheme.theme.appTitleStyle.copyWith( 45 | fontSize: 30, 46 | fontWeight: FontWeight.w800, 47 | ), 48 | ), 49 | ) 50 | ], 51 | ), 52 | ), 53 | Expanded( 54 | flex: 1, 55 | child: Column( 56 | crossAxisAlignment: CrossAxisAlignment.center, 57 | mainAxisAlignment: MainAxisAlignment.center, 58 | children: [ 59 | Container( 60 | constraints: const BoxConstraints(maxWidth: 500), 61 | child: RichText( 62 | textAlign: TextAlign.center, 63 | text: TextSpan( 64 | children: [ 65 | TextSpan( 66 | text: 'Read our ', 67 | ), 68 | TextSpan( 69 | text: 'Privacy Policy. ', 70 | style: linkTheme, 71 | recognizer: TapGestureRecognizer() 72 | ..onTap = () => launchUrl( 73 | config.get('privacy_policy'), 74 | ), 75 | ), 76 | TextSpan( 77 | text: 'Tap "Agree and continue" to accept the ', 78 | ), 79 | TextSpan( 80 | text: 'Terms of Service', 81 | style: linkTheme, 82 | recognizer: TapGestureRecognizer() 83 | ..onTap = () => launchUrl( 84 | config.get('privacy_policy'), 85 | ), 86 | ) 87 | ], 88 | style: Theme.of(context) 89 | .textTheme 90 | .bodySmall 91 | ?.copyWith(fontSize: 14), 92 | ), 93 | ), 94 | ), 95 | Container( 96 | margin: const EdgeInsets.symmetric( 97 | horizontal: 20, 98 | vertical: 15, 99 | ), 100 | constraints: const BoxConstraints(maxWidth: 500), 101 | child: ElevatedButton( 102 | onPressed: () async { 103 | Navigator.push( 104 | context, 105 | MaterialPageRoute( 106 | builder: (ctx) => LoginScreen(), 107 | ), 108 | ); 109 | }, 110 | style: ElevatedButton.styleFrom( 111 | shape: const RoundedRectangleBorder( 112 | borderRadius: BorderRadius.all( 113 | Radius.circular(14), 114 | ), 115 | ), 116 | ), 117 | child: Container( 118 | padding: const EdgeInsets.symmetric( 119 | vertical: 8, 120 | horizontal: 8, 121 | ), 122 | child: Row( 123 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 124 | children: [ 125 | Text( 126 | 'AGREE AND CONTINUE', 127 | ), 128 | Container( 129 | padding: const EdgeInsets.all(8), 130 | decoration: BoxDecoration( 131 | borderRadius: const BorderRadius.all( 132 | Radius.circular(16), 133 | ), 134 | ), 135 | child: Icon( 136 | Icons.arrow_forward_ios, 137 | size: 16, 138 | ), 139 | ) 140 | ], 141 | ), 142 | ), 143 | ), 144 | ), 145 | Text( 146 | "v${config.packageInfo.version}+${config.packageInfo.buildNumber}", 147 | ) 148 | ], 149 | ), 150 | ) 151 | ], 152 | ), 153 | ), 154 | ); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /lib/screens/login/verifyOtp.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:vartalap/widgets/keyboard.dart'; 3 | import 'package:vartalap/services/user_service.dart'; 4 | 5 | class VerifyOtpWidget extends StatefulWidget { 6 | @override 7 | State createState() => _VerifyOtpState(); 8 | } 9 | 10 | class _VerifyOtpState extends State { 11 | String _otp = ''; 12 | Widget otpNumberWidget(int position) { 13 | return Container( 14 | height: 40, 15 | width: 40, 16 | decoration: BoxDecoration( 17 | border: Border.all( 18 | width: 1, 19 | color: Theme.of(context).iconTheme.color!, 20 | ), 21 | borderRadius: const BorderRadius.all( 22 | Radius.circular(8), 23 | ), 24 | ), 25 | child: (_otp.length < (position + 1)) 26 | ? null 27 | : Center( 28 | child: Text( 29 | _otp[position], 30 | )), 31 | ); 32 | } 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | return Scaffold( 37 | appBar: AppBar( 38 | backgroundColor: Colors.transparent, 39 | elevation: 0, 40 | iconTheme: Theme.of(context).iconTheme, 41 | ), 42 | body: Padding( 43 | padding: const EdgeInsets.all(10.0), 44 | child: Column( 45 | children: [ 46 | Expanded( 47 | flex: 1, 48 | child: Column( 49 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 50 | children: [ 51 | Container( 52 | child: Text( 53 | 'Enter 6 digits verification code sent to your number', 54 | style: TextStyle( 55 | fontSize: 20, 56 | fontWeight: FontWeight.w500, 57 | ), 58 | overflow: TextOverflow.clip, 59 | ), 60 | ), 61 | Container( 62 | constraints: const BoxConstraints(maxWidth: 500), 63 | child: Row( 64 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 65 | crossAxisAlignment: CrossAxisAlignment.center, 66 | children: [ 67 | otpNumberWidget(0), 68 | otpNumberWidget(1), 69 | otpNumberWidget(2), 70 | otpNumberWidget(3), 71 | otpNumberWidget(4), 72 | otpNumberWidget(5), 73 | ], 74 | ), 75 | ), 76 | ], 77 | ), 78 | ), 79 | Expanded( 80 | flex: 2, 81 | child: Column( 82 | children: [ 83 | Container( 84 | margin: const EdgeInsets.symmetric( 85 | horizontal: 20, 86 | vertical: 10, 87 | ), 88 | constraints: const BoxConstraints(maxWidth: 500), 89 | child: ElevatedButton( 90 | onPressed: () async { 91 | bool result = await UserService.authenicate(this._otp); 92 | if (!result) { 93 | showErrorDialog(context, 94 | ['Incorrect one time password! Try again']); 95 | return; 96 | } 97 | }, 98 | style: ElevatedButton.styleFrom( 99 | shape: const RoundedRectangleBorder( 100 | borderRadius: 101 | BorderRadius.all(Radius.circular(14))), 102 | ), 103 | child: Container( 104 | padding: const EdgeInsets.symmetric( 105 | vertical: 8, horizontal: 8), 106 | child: Row( 107 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 108 | children: [ 109 | Text( 110 | 'Confirm', 111 | style: TextStyle(color: Colors.white), 112 | ), 113 | Container( 114 | padding: const EdgeInsets.all(8), 115 | decoration: BoxDecoration( 116 | borderRadius: 117 | const BorderRadius.all(Radius.circular(20)), 118 | ), 119 | child: Icon( 120 | Icons.arrow_forward_ios, 121 | size: 16, 122 | ), 123 | ) 124 | ], 125 | ), 126 | ), 127 | ), 128 | ), 129 | Expanded( 130 | child: NumericKeyboard( 131 | onKeyboardTap: _onKeyboardTap, 132 | textColor: Theme.of(context).iconTheme.color!, 133 | rightIcon: Icon( 134 | Icons.backspace, 135 | ), 136 | rightButtonFn: () { 137 | if (_otp.length > 0) { 138 | setState(() { 139 | _otp = _otp.substring(0, _otp.length - 1); 140 | }); 141 | } 142 | }, 143 | ), 144 | ), 145 | ], 146 | ), 147 | ) 148 | ], 149 | ), 150 | ), 151 | ); 152 | } 153 | 154 | void _onKeyboardTap(String value) { 155 | if (_otp.length == 6) return; 156 | setState(() { 157 | _otp = _otp + value; 158 | }); 159 | } 160 | 161 | void showErrorDialog(BuildContext context, List messages) { 162 | var contents = messages.map((e) => Text(e)).toList(); 163 | var dialog = AlertDialog( 164 | title: Text("Error"), 165 | content: SingleChildScrollView( 166 | child: ListBody(children: contents), 167 | ), 168 | actions: [ 169 | TextButton( 170 | child: Text('OK'), 171 | onPressed: () { 172 | Navigator.of(context).pop(); 173 | }, 174 | ), 175 | ], 176 | ); 177 | 178 | showDialog( 179 | context: context, 180 | builder: (context) => dialog, 181 | ); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /lib/services/api_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:flutter_secure_storage/flutter_secure_storage.dart'; 3 | import 'package:http/http.dart' as http; 4 | import 'package:vartalap/config/config_store.dart'; 5 | import 'package:vartalap/models/remoteMessage.dart'; 6 | import 'package:vartalap/services/auth_service.dart'; 7 | import 'package:vartalap/services/push_notification_service.dart'; 8 | 9 | import 'package:vartalap/services/performance_metric.dart'; 10 | 11 | class ApiService { 12 | static FlutterSecureStorage _storage = new FlutterSecureStorage(); 13 | static const String ACCESS_KEY = 'accesskey'; 14 | static Future get _accesskey { 15 | return _storage.read(key: ACCESS_KEY); 16 | } 17 | 18 | static Future> getAuthHeader( 19 | {includeAccessKey = true}) async { 20 | String? idToken = await AuthService.instance.idToken; 21 | Map headers = {}; 22 | if (idToken != null) { 23 | headers["token"] = idToken; 24 | } 25 | String? phone = AuthService.instance.phoneNumber; 26 | if (phone != null) { 27 | headers["user"] = phone; 28 | } 29 | if (includeAccessKey) { 30 | String? key = await _accesskey; 31 | if (key != null) headers[ACCESS_KEY] = key; 32 | } 33 | 34 | return headers; 35 | } 36 | 37 | static Future _post(String path, dynamic data, 38 | {bool includeAccesskey = true}) async { 39 | String baseUrl = ConfigStore().get("api_url"); 40 | var resourceUrl = Uri.parse("$baseUrl/$path"); 41 | var _httpMetric = 42 | PerformanceMetric.newHttpMetric(resourceUrl.toString(), 'post'); 43 | 44 | String content = json.encode(data); 45 | Map headers = 46 | await getAuthHeader(includeAccessKey: includeAccesskey); 47 | headers["Content-Type"] = "application/json"; 48 | 49 | await _httpMetric.start(); 50 | http.Response resp; 51 | try { 52 | resp = await http.post(resourceUrl, headers: headers, body: content); 53 | _httpMetric 54 | ..responsePayloadSize = resp.contentLength ?? 0 55 | ..responseContentType = resp.headers['Content-Type'] ?? '' 56 | ..requestPayloadSize = resp.contentLength ?? 0 57 | ..httpResponseCode = resp.statusCode; 58 | } finally { 59 | _httpMetric.stop(); 60 | } 61 | 62 | return resp; 63 | } 64 | 65 | static Future _get(String path, 66 | {bool includeAccesskey = true}) async { 67 | String baseUrl = ConfigStore().get("api_url"); 68 | var resourceUrl = Uri.parse("$baseUrl/$path"); 69 | var _httpMetric = 70 | PerformanceMetric.newHttpMetric(resourceUrl.toString(), 'get'); 71 | 72 | Map headers = 73 | await getAuthHeader(includeAccessKey: includeAccesskey); 74 | 75 | await _httpMetric.start(); 76 | http.Response resp; 77 | try { 78 | resp = await http.get(resourceUrl, headers: headers); 79 | _httpMetric 80 | ..responsePayloadSize = resp.contentLength ?? 0 81 | ..responseContentType = resp.headers['Content-Type'] ?? '' 82 | ..requestPayloadSize = resp.contentLength ?? 0 83 | ..httpResponseCode = resp.statusCode; 84 | } finally { 85 | _httpMetric.stop(); 86 | } 87 | 88 | return resp; 89 | } 90 | 91 | static Map _handleResponse(http.Response resp) { 92 | if (resp.statusCode >= 200 && resp.statusCode < 300) { 93 | Map response; 94 | if (resp.body.length > 0) { 95 | var decoded = json.decode(resp.body); 96 | response = Map.from(decoded); 97 | } else { 98 | response = {}; 99 | } 100 | return response; 101 | } 102 | throw Exception("Response code ${resp.statusCode}"); 103 | } 104 | 105 | static List> _handleListResponse(http.Response resp) { 106 | if (resp.statusCode == 200) { 107 | var decoded = json.decode(resp.body); 108 | List> response = []; 109 | if (decoded is List) { 110 | for (var i = 0; i < decoded.length; i++) { 111 | var resp = Map.from(decoded[i]); 112 | response.add(resp); 113 | } 114 | } 115 | return response; 116 | } 117 | throw Exception("Response code ${resp.statusCode}"); 118 | } 119 | 120 | static login(String phone) async { 121 | var notificationToken = await PushNotificationService.instance.token; 122 | http.Response response = await _post( 123 | "login", 124 | { 125 | "username": phone, 126 | "notificationToken": notificationToken, 127 | }, 128 | includeAccesskey: false); 129 | Map resp = _handleResponse(response); 130 | String accessKey = resp["accesskey"]; 131 | _storage.write(key: ACCESS_KEY, value: accessKey); 132 | } 133 | 134 | static Future> syncContact(List users) async { 135 | http.Response response = await _post("profile/user/sync", {"users": users}); 136 | Map resp = _handleResponse(response); 137 | return resp; 138 | } 139 | 140 | static Future createGroup( 141 | String groupTitle, List members, String? profilePic) async { 142 | http.Response response = await _post("group/create", 143 | {"name": groupTitle, "members": members, "profilePic": profilePic}); 144 | Map resp = _handleResponse(response); 145 | return resp["groupId"].toString(); 146 | } 147 | 148 | static Future addMembersToGroup( 149 | List members, String groupId) async { 150 | http.Response response = 151 | await _post("group/$groupId/add", {"members": members}); 152 | Map resp = _handleResponse(response); 153 | return resp["status"]; 154 | } 155 | 156 | static Future removeMemberToGroup(String member, String groupId) async { 157 | http.Response response = 158 | await _post("group/$groupId/remove", {"member": member}); 159 | Map resp = _handleResponse(response); 160 | return resp["status"]; 161 | } 162 | 163 | static Future> getGroupInfo(String groupId) async { 164 | http.Response response = await _get("group/$groupId"); 165 | Map resp = _handleResponse(response); 166 | return resp; 167 | } 168 | 169 | static Future>> getGroups() async { 170 | http.Response response = await _get("group/get"); 171 | var resp = _handleListResponse(response); 172 | return resp; 173 | } 174 | 175 | static Future sendMessages(Iterable messages) async { 176 | final body = messages.map((msg) => json.encode(msg.toMap())).toList(); 177 | final resp = await _post("messages", body); 178 | _handleResponse(resp); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /lib/widgets/message_input.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:vartalap/theme/theme.dart'; 6 | 7 | class MessageInputWidget extends StatefulWidget { 8 | final Function sendMessage; 9 | final Function(bool state)? onTyping; 10 | MessageInputWidget({ 11 | Key? key, 12 | required this.sendMessage, 13 | this.onTyping, 14 | }) : super(key: key); 15 | 16 | @override 17 | MessageInputState createState() => MessageInputState(); 18 | } 19 | 20 | class MessageInputState extends State { 21 | late Function _sendMessage; 22 | bool _isShowSticker = false; 23 | FocusNode _inputFocus = FocusNode(); 24 | Timer? _typingTimer; 25 | final TextEditingController _controller = TextEditingController(); 26 | @override 27 | void initState() { 28 | super.initState(); 29 | _sendMessage = widget.sendMessage; 30 | _isShowSticker = false; 31 | _inputFocus = FocusNode(); 32 | _inputFocus.addListener(onFocusListener); 33 | _controller.addListener(onTypingListener); 34 | } 35 | 36 | void dispose() { 37 | super.dispose(); 38 | if (_typingTimer?.isActive ?? false) _typingTimer!.cancel(); 39 | _inputFocus.removeListener(onFocusListener); 40 | _controller.removeListener(onTypingListener); 41 | _inputFocus.dispose(); 42 | _controller.dispose(); 43 | } 44 | 45 | void onTypingListener() { 46 | if (this._controller.text.isEmpty) return; 47 | if (_typingTimer == null) { 48 | this.widget.onTyping?.call(true); 49 | } 50 | if (_typingTimer?.isActive ?? false) _typingTimer!.cancel(); 51 | _typingTimer = Timer(Duration(seconds: 3), onTypingTimeout); 52 | } 53 | 54 | void onTypingTimeout() { 55 | this.widget.onTyping?.call(false); 56 | _typingTimer = null; 57 | } 58 | 59 | void onFocusListener() { 60 | if (_isShowSticker && _inputFocus.hasFocus) { 61 | setState(() { 62 | _isShowSticker = false; 63 | }); 64 | } 65 | } 66 | 67 | Future onBackPress() { 68 | if (_isShowSticker) { 69 | setState(() { 70 | _isShowSticker = false; 71 | }); 72 | } else { 73 | Navigator.pop(context); 74 | } 75 | 76 | return Future.value(false); 77 | } 78 | 79 | @override 80 | Widget build(BuildContext context) { 81 | return WillPopScope( 82 | onWillPop: onBackPress, 83 | child: Stack( 84 | children: [ 85 | Column( 86 | children: [buildInput(context), buildSticker(context)], 87 | ), 88 | ], 89 | ), 90 | ); 91 | } 92 | 93 | Widget buildInput(BuildContext context) { 94 | var theme = Theme.of(context); 95 | return Container( 96 | child: Row( 97 | mainAxisSize: MainAxisSize.max, 98 | children: [ 99 | Flexible( 100 | flex: 1, 101 | child: Container( 102 | decoration: BoxDecoration( 103 | color: theme.primaryColorLight, 104 | //borderRadius: BorderRadius.all(const Radius.circular(30.0)), 105 | ), 106 | child: Row( 107 | children: [ 108 | IconButton( 109 | padding: const EdgeInsets.all(0.0), 110 | icon: Icon(_isShowSticker 111 | ? Icons.keyboard 112 | : Icons.insert_emoticon_sharp), 113 | onPressed: () { 114 | _isShowSticker 115 | ? _inputFocus.requestFocus() 116 | : _inputFocus.unfocus(); 117 | setState(() { 118 | _isShowSticker = !_isShowSticker; 119 | }); 120 | }, 121 | ), 122 | Flexible( 123 | child: TextField( 124 | controller: _controller, 125 | textCapitalization: TextCapitalization.sentences, 126 | textInputAction: TextInputAction.send, 127 | decoration: InputDecoration( 128 | border: InputBorder.none, 129 | contentPadding: const EdgeInsets.all(0.0), 130 | hintText: 'Type a message', 131 | hintStyle: TextStyle( 132 | fontSize: 16.0, 133 | ), 134 | counterText: '', 135 | ), 136 | onSubmitted: (String text) { 137 | sendMessage(); 138 | }, 139 | keyboardType: TextInputType.multiline, 140 | style: TextStyle( 141 | fontSize: 19, 142 | ), 143 | maxLines: null, 144 | maxLength: TextField.noMaxLength, 145 | focusNode: _inputFocus, 146 | ), 147 | ), 148 | // IconButton( 149 | // icon: Icon(Icons.attach_file), 150 | // onPressed: () {}, 151 | // ), 152 | IconButton( 153 | onPressed: sendMessage, 154 | icon: Icon(Icons.send_rounded), 155 | ), 156 | ], 157 | ), 158 | ), 159 | ), 160 | ], 161 | ), 162 | ); 163 | } 164 | 165 | Widget buildSticker(BuildContext context) { 166 | var theme = Theme.of(context); 167 | final vtheme = VartalapTheme.theme; 168 | return Offstage( 169 | offstage: !_isShowSticker, 170 | child: SizedBox( 171 | height: 250, 172 | child: EmojiPicker( 173 | onEmojiSelected: (category, emoji) { 174 | _controller..text += emoji.emoji; 175 | }, 176 | config: Config( 177 | columns: 8, 178 | emojiSizeMax: 25.0, 179 | verticalSpacing: 0, 180 | horizontalSpacing: 0, 181 | initCategory: Category.RECENT, 182 | bgColor: theme.scaffoldBackgroundColor, 183 | indicatorColor: theme.indicatorColor, 184 | recentsLimit: 28, 185 | enableSkinTones: true, 186 | categoryIcons: CategoryIcons(), 187 | buttonMode: ButtonMode.MATERIAL, 188 | iconColorSelected: vtheme.selectedRowColor, 189 | ), 190 | ), 191 | ), 192 | ); 193 | } 194 | 195 | void sendMessage() { 196 | var text = this._controller.text; 197 | if (text.length == 0) { 198 | return; 199 | } 200 | _sendMessage(text); 201 | this._controller.text = ""; 202 | if (_isShowSticker) { 203 | setState(() { 204 | _isShowSticker = false; 205 | }); 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /lib/screens/new_chat/create_group.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:vartalap/models/user.dart'; 3 | import 'package:vartalap/services/chat_service.dart'; 4 | import 'package:vartalap/widgets/avator.dart'; 5 | import 'package:vartalap/widgets/contactPreviewItem.dart'; 6 | import 'package:vartalap/widgets/loadingIndicator.dart'; 7 | 8 | class CreateGroup extends StatelessWidget { 9 | final List _members; 10 | CreateGroup(this._members); 11 | @override 12 | Widget build(BuildContext context) { 13 | onGroupNameConfirm(String name) async { 14 | if (name.isNotEmpty) { 15 | try { 16 | showLoadingIndicator(context); 17 | var chat = await ChatService.newGroupChat(name, this._members); 18 | Navigator.of(context).pop(); 19 | Navigator.of(context).pop(chat); 20 | } on Exception catch (_) { 21 | showErrorDialog(context, [ 22 | 'Error while creating new group.', 23 | 'Make sure you are connected to internet.' 24 | ]); 25 | } 26 | } 27 | } 28 | 29 | return Scaffold( 30 | appBar: AppBar( 31 | title: Column( 32 | mainAxisSize: MainAxisSize.max, 33 | mainAxisAlignment: MainAxisAlignment.center, 34 | crossAxisAlignment: CrossAxisAlignment.start, 35 | children: [ 36 | Padding( 37 | padding: const EdgeInsets.only(bottom: 2.0), 38 | child: Text( 39 | 'New Group', 40 | style: TextStyle( 41 | fontWeight: FontWeight.bold, 42 | ), 43 | ), 44 | ), 45 | ], 46 | ), 47 | ), 48 | body: SafeArea( 49 | child: Container( 50 | margin: EdgeInsets.only(left: 20, top: 10), 51 | child: Column( 52 | crossAxisAlignment: CrossAxisAlignment.start, 53 | children: [ 54 | Container( 55 | child: _CreateGroupForm( 56 | onConfirm: onGroupNameConfirm, 57 | ), 58 | ), 59 | Container( 60 | child: RichText( 61 | text: TextSpan( 62 | style: TextStyle( 63 | fontSize: 18, 64 | color: Theme.of(context).textTheme.bodyLarge!.color, 65 | ), 66 | children: [ 67 | TextSpan(text: "Members:"), 68 | TextSpan( 69 | text: _members.length.toString(), 70 | ) 71 | ], 72 | ), 73 | ), 74 | ), 75 | Expanded( 76 | flex: 10, 77 | child: GridView.count( 78 | padding: EdgeInsets.symmetric(vertical: 10), 79 | crossAxisCount: 5, 80 | childAspectRatio: 0.5, 81 | children: 82 | _members.map((e) => ContactPreviewItem(user: e)).toList(), 83 | ), 84 | ) 85 | ], 86 | ), 87 | ), 88 | ), 89 | ); 90 | } 91 | 92 | void showLoadingIndicator(BuildContext context) { 93 | showDialog( 94 | context: context, 95 | barrierDismissible: false, 96 | builder: (BuildContext context) { 97 | return WillPopScope( 98 | onWillPop: () async => false, 99 | child: AlertDialog( 100 | content: LoadingIndicator( 101 | text: "While we are creating group for you.", 102 | ), 103 | ), 104 | ); 105 | }, 106 | ); 107 | } 108 | 109 | void showErrorDialog(BuildContext context, List error) { 110 | var dialog = AlertDialog( 111 | title: Text('Error'), 112 | content: SingleChildScrollView( 113 | child: ListBody( 114 | children: error.map((err) => Text(err)).toList(), 115 | ), 116 | ), 117 | actions: [ 118 | TextButton( 119 | child: Text('OK'), 120 | onPressed: () { 121 | Navigator.of(context).pop(); 122 | }, 123 | ), 124 | ], 125 | ); 126 | showDialog( 127 | context: context, 128 | builder: (context) => dialog, 129 | ); 130 | } 131 | } 132 | 133 | class _CreateGroupForm extends StatefulWidget { 134 | final Function(String) onConfirm; 135 | _CreateGroupForm({Key? key, required this.onConfirm}) : super(key: key); 136 | 137 | @override 138 | __CreateGroupFormState createState() => __CreateGroupFormState(); 139 | } 140 | 141 | class __CreateGroupFormState extends State<_CreateGroupForm> { 142 | String value = ""; 143 | @override 144 | Widget build(BuildContext context) { 145 | return Container( 146 | decoration: BoxDecoration( 147 | shape: BoxShape.rectangle, 148 | ), 149 | child: Column( 150 | crossAxisAlignment: CrossAxisAlignment.start, 151 | children: [ 152 | ListTile( 153 | leading: Avator( 154 | width: 55, 155 | height: 55, 156 | text: value.isEmpty ? "Group Icon" : value, 157 | ), 158 | title: Padding( 159 | padding: EdgeInsets.symmetric(horizontal: 10), 160 | child: TextField( 161 | maxLines: 1, 162 | autofocus: true, 163 | textCapitalization: TextCapitalization.words, 164 | style: TextStyle( 165 | fontSize: 18, 166 | ), 167 | onChanged: (val) { 168 | setState(() { 169 | this.value = val; 170 | }); 171 | }, 172 | ), 173 | ), 174 | subtitle: Container(), 175 | ), 176 | Padding( 177 | padding: const EdgeInsets.all(8.0), 178 | child: Row( 179 | mainAxisAlignment: MainAxisAlignment.end, 180 | children: [ 181 | TextButton( 182 | onPressed: () async { 183 | if (value.isNotEmpty) { 184 | this.widget.onConfirm(value); 185 | } else { 186 | final snackBar = SnackBar( 187 | content: Text('Group name can\'t be empty!')); 188 | ScaffoldMessenger.of(context).showSnackBar(snackBar); 189 | } 190 | }, 191 | style: TextButton.styleFrom( 192 | backgroundColor: Theme.of(context).primaryColor, 193 | shape: const CircleBorder(side: BorderSide.none), 194 | ), 195 | child: Container( 196 | padding: 197 | const EdgeInsets.symmetric(vertical: 8, horizontal: 8), 198 | child: Icon( 199 | Icons.done, 200 | //color: Colors.white, 201 | size: 30, 202 | ), 203 | ), 204 | ), 205 | ], 206 | ), 207 | ) 208 | ], 209 | ), 210 | ); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /lib/widgets/chatlist.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:vartalap/models/chat.dart'; 3 | import 'package:vartalap/models/dateHeader.dart'; 4 | import 'package:vartalap/models/message.dart'; 5 | import 'package:vartalap/models/messageSpacer.dart'; 6 | import 'package:vartalap/models/user.dart'; 7 | import 'package:vartalap/widgets/message.dart'; 8 | import 'package:vartalap/services/user_service.dart'; 9 | import 'package:vartalap/theme/theme.dart'; 10 | import 'package:vartalap/utils/chat_message_helper.dart'; 11 | import 'package:vartalap/widgets/Inherited/current_user.dart'; 12 | 13 | class ChatMessageController extends ValueNotifier> { 14 | final Map messageChangeNotifier = {}; 15 | 16 | ChatMessageController({required List messages}) 17 | : super(messages); 18 | 19 | getNewNotifier(ChatMessage msg) { 20 | var notifier = this.messageChangeNotifier[msg.id]; 21 | if (notifier == null) { 22 | notifier = ChatMessageNotifier(msg); 23 | this.messageChangeNotifier[msg.id] = notifier; 24 | } 25 | 26 | return notifier; 27 | } 28 | 29 | add(ChatMessage msg) { 30 | this.messageChangeNotifier[msg.id] = ChatMessageNotifier(msg); 31 | this.value.insert(0, msg); 32 | this.notifyListeners(); 33 | } 34 | 35 | addAll(Iterable msgs) { 36 | msgs.forEach((msg) { 37 | this.messageChangeNotifier[msg.id] = ChatMessageNotifier(msg); 38 | }); 39 | this.value.insertAll(0, msgs); 40 | this.notifyListeners(); 41 | } 42 | 43 | delete(String id) { 44 | this.value.removeWhere((msg) => msg.id == id); 45 | this.messageChangeNotifier.remove(id); 46 | this.notifyListeners(); 47 | } 48 | 49 | deleteAll(Iterable ids) { 50 | this.value.removeWhere((ChatMessage msg) => ids.contains(msg.id)); 51 | ids.forEach((id) { 52 | this.messageChangeNotifier.remove(id); 53 | }); 54 | this.notifyListeners(); 55 | } 56 | 57 | update(ChatMessage msg) { 58 | int idx = this.value.indexWhere((message) => message.id == msg.id); 59 | if (idx == -1) return; 60 | this.value[idx] = msg; 61 | final notifier = this.messageChangeNotifier[msg.id]; 62 | if (notifier != null) { 63 | notifier.update(msg); 64 | } 65 | } 66 | 67 | updateAll(Iterable msgs) { 68 | msgs.forEach(this.update); 69 | } 70 | 71 | @override 72 | void dispose() { 73 | super.dispose(); 74 | this.messageChangeNotifier.values.forEach((msgNotifier) { 75 | msgNotifier.dispose(); 76 | }); 77 | } 78 | } 79 | 80 | typedef MessageTapCallback = void Function(ChatMessage msg); 81 | typedef MessageLongPressCallback = void Function(ChatMessage msg); 82 | 83 | class ChatList extends StatelessWidget { 84 | final ChatMessageController controller; 85 | final bool showName; 86 | final MessageTapCallback? onTab; 87 | final MessageLongPressCallback? onLongPress; 88 | final Map users; 89 | final Map _userChangeNotifier = {}; 90 | ChatList({ 91 | Key? key, 92 | required this.controller, 93 | required this.users, 94 | this.showName = false, 95 | this.onLongPress, 96 | this.onTab, 97 | }) : super(key: key); 98 | 99 | @override 100 | Widget build(BuildContext context) { 101 | final currentUser = CurrentUser.of(context).user!; 102 | return ValueListenableBuilder( 103 | valueListenable: this.controller, 104 | builder: (BuildContext context, List value, Widget? child) { 105 | final displayMessages = calculateChatMessages( 106 | value, 107 | currentUser, 108 | showUserNames: this.showName, 109 | )[0] as List; 110 | return ListView.builder( 111 | itemCount: displayMessages.length, 112 | reverse: true, 113 | itemBuilder: (context, i) { 114 | return _messageBuilder(displayMessages[i], currentUser); 115 | }, 116 | ); 117 | }, 118 | ); 119 | } 120 | 121 | Widget _messageBuilder(Object object, User currentUser) { 122 | if (object is DateHeader) { 123 | return Container( 124 | alignment: Alignment.center, 125 | margin: const EdgeInsets.only( 126 | bottom: 32, 127 | top: 16, 128 | ), 129 | child: Container( 130 | padding: const EdgeInsets.all(5), 131 | decoration: BoxDecoration( 132 | color: VartalapTheme.theme.receiverColor, 133 | borderRadius: BorderRadius.all( 134 | Radius.circular(5), 135 | ), 136 | ), 137 | child: Text( 138 | object.date, 139 | ), 140 | ), 141 | ); 142 | } else if (object is MessageSpacer) { 143 | return SizedBox( 144 | height: object.height, 145 | ); 146 | } else if (object is Map) { 147 | ChatMessage msg = object["message"]; 148 | bool showName = object["showName"]; 149 | bool showNip = object["showNip"]; 150 | bool isYou = msg.sender == currentUser; 151 | bool showUserInfo = !isYou && this.showName && showName; 152 | if (msg.sender == null) { 153 | msg.sender = this._getSender(msg.senderId); 154 | } 155 | final notifier = this.controller.getNewNotifier(msg); 156 | Widget child = ValueListenableBuilder( 157 | builder: (context, key, child) { 158 | if (this._userChangeNotifier.containsKey(msg.senderId)) { 159 | return ValueListenableBuilder( 160 | valueListenable: this._userChangeNotifier[msg.senderId]!, 161 | builder: (BuildContext context, User sender, Widget? child) { 162 | msg.sender = sender; 163 | return MessageWidget( 164 | msg, 165 | isYou, 166 | showUserInfo: showUserInfo, 167 | isSelected: msg.isSelected, 168 | showNip: showNip, 169 | onTab: this.onTab, 170 | onLongPress: this.onLongPress, 171 | ); 172 | }, 173 | ); 174 | } 175 | return MessageWidget( 176 | msg, 177 | isYou, 178 | showUserInfo: showUserInfo, 179 | isSelected: msg.isSelected, 180 | showNip: showNip, 181 | onTab: this.onTab, 182 | onLongPress: this.onLongPress, 183 | ); 184 | }, 185 | valueListenable: notifier, 186 | ); 187 | 188 | return child; 189 | } 190 | return const SizedBox(); 191 | } 192 | 193 | User _getSender(String senderId) { 194 | if (this.users.containsKey(senderId)) { 195 | return this.users[senderId]!; 196 | } else if (this._userChangeNotifier.containsKey(senderId)) { 197 | return this._userChangeNotifier[senderId]!.value; 198 | } else { 199 | final user = User(senderId, senderId, null); 200 | this._userChangeNotifier[senderId] = UserNotifier(user); 201 | UserService.getUserById(senderId).then((User? user) { 202 | if (user == null) return; 203 | this.users[user.username] = ChatUser.fromUser(user); 204 | this._userChangeNotifier[senderId]!.update(user); 205 | }, onError: (user) {}); 206 | return user; 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /lib/screens/new_chat/select_group_member.dart: -------------------------------------------------------------------------------- 1 | import 'package:vartalap/models/chat.dart'; 2 | import 'package:vartalap/models/user.dart'; 3 | import 'package:vartalap/services/user_service.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:vartalap/widgets/contactPreviewItem.dart'; 6 | import 'package:vartalap/widgets/contact.dart'; 7 | 8 | class SelectGroupMemberScreen extends StatefulWidget { 9 | final Chat? chat; 10 | SelectGroupMemberScreen({this.chat}); 11 | @override 12 | State createState() => SelectGroupMemberState(); 13 | } 14 | 15 | class SelectGroupMemberState extends State { 16 | late Future> _contacts; 17 | late int _numContacts; 18 | bool _openSearch = false; 19 | List _selectedUsers = []; 20 | Map _existingUser = Map(); 21 | bool _isUpdate = false; 22 | @override 23 | void initState() { 24 | super.initState(); 25 | if (this.widget.chat != null) { 26 | this._isUpdate = true; 27 | this 28 | .widget 29 | .chat! 30 | .users 31 | .forEach((u) => this._existingUser[u.username] = u); 32 | } 33 | _contacts = UserService.getUsers(); 34 | _contacts.then((value) { 35 | setState(() { 36 | _numContacts = value.length; 37 | }); 38 | }); 39 | } 40 | 41 | @override 42 | Widget build(BuildContext context) { 43 | return new Scaffold( 44 | appBar: this._openSearch ? buildSearchAppBar() : buildAppBar(), 45 | body: Column( 46 | children: [ 47 | ...(this._selectedUsers.length > 0 48 | ? [ 49 | SizedBox( 50 | height: 90, 51 | child: ListView.separated( 52 | padding: EdgeInsets.only(top: 10, left: 20), 53 | scrollDirection: Axis.horizontal, 54 | shrinkWrap: true, 55 | reverse: true, 56 | itemCount: this._selectedUsers.length, 57 | separatorBuilder: (context, index) => SizedBox( 58 | width: 10, 59 | ), 60 | itemBuilder: (context, index) { 61 | return ContactPreviewItem(user: _selectedUsers[index]); 62 | }, 63 | ), 64 | ), 65 | Divider( 66 | thickness: 2, 67 | ), 68 | ] 69 | : []), 70 | Flexible( 71 | child: FutureBuilder>( 72 | future: _contacts, 73 | builder: (context, snapshot) { 74 | switch (snapshot.connectionState) { 75 | case ConnectionState.none: 76 | return Center( 77 | child: CircularProgressIndicator( 78 | valueColor: 79 | new AlwaysStoppedAnimation(Colors.grey), 80 | ), 81 | ); 82 | case ConnectionState.active: 83 | case ConnectionState.waiting: 84 | return Center( 85 | child: CircularProgressIndicator( 86 | valueColor: 87 | new AlwaysStoppedAnimation(Colors.grey), 88 | ), 89 | ); 90 | case ConnectionState.done: 91 | if (snapshot.hasError) { 92 | return Center( 93 | child: Text('Error: ${snapshot.error}'), 94 | ); 95 | } 96 | } 97 | List data = snapshot.data!.toList(); 98 | return ListView.builder( 99 | itemCount: data.length, 100 | itemBuilder: (context, i) { 101 | User user = data.elementAt(i); 102 | return ContactItem( 103 | user: user, 104 | isSelected: this._selectedUsers.contains(user), 105 | enabled: !this._existingUser.containsKey(user.username), 106 | onProfileTap: () => {}, 107 | onTap: (User user) async { 108 | if (this._existingUser.containsKey(user.username)) 109 | return; 110 | setState(() { 111 | if (!this._selectedUsers.remove(user)) { 112 | this._selectedUsers.add(user); 113 | } 114 | }); 115 | }, 116 | ); 117 | }, 118 | ); 119 | }, 120 | ), 121 | ), 122 | ], 123 | ), 124 | floatingActionButton: this._selectedUsers.length > 0 125 | ? FloatingActionButton( 126 | onPressed: () async { 127 | if (this._isUpdate) { 128 | return Navigator.of(context).pop(_selectedUsers); 129 | } 130 | var chat = await Navigator.of(context) 131 | .pushNamed('/create-group', arguments: _selectedUsers); 132 | if (chat is Chat) { 133 | Navigator.of(context).popAndPushNamed( 134 | '/chat', 135 | arguments: chat, 136 | ); 137 | } 138 | }, 139 | tooltip: 'Next', 140 | child: Icon(this._isUpdate ? Icons.check : Icons.arrow_forward), 141 | ) 142 | : null, 143 | ); 144 | } 145 | 146 | AppBar buildAppBar() { 147 | return AppBar( 148 | title: Column( 149 | mainAxisSize: MainAxisSize.max, 150 | mainAxisAlignment: MainAxisAlignment.center, 151 | crossAxisAlignment: CrossAxisAlignment.start, 152 | children: [ 153 | Padding( 154 | padding: const EdgeInsets.only(bottom: 2.0), 155 | child: Text( 156 | 'Select members', 157 | style: TextStyle( 158 | fontWeight: FontWeight.bold, 159 | ), 160 | ), 161 | ), 162 | _selectedUsers.isEmpty 163 | ? Container() 164 | : Container( 165 | child: Text( 166 | '${_selectedUsers.length} of $_numContacts', 167 | style: TextStyle( 168 | fontSize: 12.0, 169 | ), 170 | ), 171 | ) 172 | ], 173 | ), 174 | actions: [ 175 | IconButton( 176 | tooltip: 'Search', 177 | icon: Icon(Icons.search), 178 | onPressed: () { 179 | setState(() { 180 | this._openSearch = true; 181 | }); 182 | }, 183 | ), 184 | ], 185 | ); 186 | } 187 | 188 | AppBar buildSearchAppBar() { 189 | return AppBar( 190 | leading: TextButton( 191 | style: TextButton.styleFrom( 192 | shape: CircleBorder(), 193 | padding: const EdgeInsets.only(left: 1.0), 194 | ), 195 | onPressed: () { 196 | setState(() { 197 | this._openSearch = false; 198 | this._contacts = UserService.getUsers(); 199 | }); 200 | }, 201 | child: Icon( 202 | Icons.arrow_back, 203 | size: 24.0, 204 | color: Colors.white, 205 | ), 206 | ), 207 | titleSpacing: 0, 208 | automaticallyImplyLeading: false, 209 | title: TextField( 210 | style: TextStyle( 211 | fontSize: 20.0, 212 | color: Colors.white, 213 | ), 214 | decoration: InputDecoration( 215 | border: InputBorder.none, 216 | hintText: "Search", 217 | hintStyle: TextStyle( 218 | fontSize: 20.0, 219 | color: Colors.white, 220 | ), 221 | ), 222 | maxLines: 1, 223 | autofocus: true, 224 | onChanged: (value) { 225 | setState(() { 226 | this._contacts = UserService.getUsers(search: value); 227 | }); 228 | }, 229 | ), 230 | actions: [], 231 | ); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /lib/services/user_service.dart: -------------------------------------------------------------------------------- 1 | import 'package:permission_handler/permission_handler.dart'; 2 | import 'package:vartalap/models/user.dart'; 3 | import 'package:vartalap/dataAccessLayer/db.dart'; 4 | import 'package:flutter_contacts/flutter_contacts.dart'; 5 | import 'package:sqflite/sqflite.dart'; 6 | import 'package:vartalap/services/api_service.dart'; 7 | import 'package:vartalap/services/auth_service.dart'; 8 | import 'package:vartalap/services/crashlystics.dart'; 9 | import 'package:vartalap/services/performance_metric.dart'; 10 | import 'package:vartalap/utils/enum_helper.dart'; 11 | import 'package:vartalap/utils/find.dart'; 12 | import 'package:vartalap/utils/phone_number.dart'; 13 | 14 | class UserService { 15 | static bool _syncInProgress = false; 16 | static bool _syncOnInit = false; 17 | static User? _user; 18 | static AuthService _authService = AuthService.instance; 19 | 20 | static Future sendOTP(String phoneNumber) { 21 | return _authService.sendOtp(phoneNumber); 22 | } 23 | 24 | static Future authenicate(String otp) async { 25 | AuthResponse result = await _authService.verify(otp); 26 | if (result.status) { 27 | getLoggedInUser(); 28 | } 29 | return result.status; 30 | } 31 | 32 | static Future isAuth() { 33 | return Future(() => _authService.isLoggedIn()); 34 | } 35 | 36 | static User getLoggedInUser() { 37 | if (_user == null) { 38 | // fetch the current user 39 | _user = User("Myself", _authService.phoneNumber!, null); 40 | } 41 | return _user!; 42 | } 43 | 44 | static Future getUserById(String username) async { 45 | var db = await DB().getDb(); 46 | var userMap = 47 | await db.query('user', where: "username=?", whereArgs: [username]); 48 | if (userMap.length == 0) { 49 | return null; 50 | } 51 | return User.fromMap(userMap[0]); 52 | } 53 | 54 | static Future> getUsers({String? search, bool sync = false}) async { 55 | if (sync) { 56 | await syncContacts(); 57 | } 58 | String where = "hasAccount=? and status=?"; 59 | List whereArgs = [ 60 | 1, 61 | enumToInt(UserStatus.ACTIVE, UserStatus.values) 62 | ]; 63 | if (search != null && search.isNotEmpty) { 64 | where += " and (name like ? or username like ?)"; 65 | whereArgs.add("%" + search + "%"); 66 | whereArgs.add("%" + search + "%"); 67 | } 68 | Database db = await DB().getDb(); 69 | var userMap = await db.query('user', 70 | where: where, whereArgs: whereArgs, orderBy: 'name'); 71 | var users = userMap.map((e) => User.fromMap(e)).toList(); 72 | return users; 73 | } 74 | 75 | static Future addUser(User user) async { 76 | var db = await DB().getDb(); 77 | await db.insert("user", user.toMap(persistent: true)); 78 | } 79 | 80 | static Future addUnknowUser(List users) async { 81 | List result = []; 82 | for (final user in users) { 83 | final u = await getUserById(user.username); 84 | if (u == null) result.add(user); 85 | } 86 | final db = await DB().getDb(); 87 | Batch batch = db.batch(); 88 | result.forEach((user) { 89 | batch.insert("user", user.toMap(persistent: true)); 90 | }); 91 | await batch.commit(); 92 | return true; 93 | } 94 | 95 | static Future syncContacts({bool onInit = false}) async { 96 | if (onInit && _syncOnInit) return; 97 | if (onInit) { 98 | _syncOnInit = true; 99 | } 100 | if (_syncInProgress) return; 101 | _syncInProgress = true; 102 | var syncContactTrace = PerformanceMetric.newTrace('sync-contact'); 103 | await syncContactTrace.start(); 104 | var users = await _getContacts(); 105 | if (users.isEmpty) { 106 | syncContactTrace.putAttribute("nocontacts", true); 107 | syncContactTrace.stop(); 108 | _syncInProgress = false; 109 | return; 110 | } 111 | var currentUser = UserService.getLoggedInUser(); 112 | if (!users.any((user) => user.username == currentUser.username)) { 113 | users.add(currentUser); 114 | } 115 | try { 116 | var result = 117 | await ApiService.syncContact(users.map((e) => e.username).toList()); 118 | users.forEach((user) { 119 | user.hasAccount = 120 | result.containsKey(user.username) ? result[user.username] : false; 121 | }); 122 | } catch (e, stack) { 123 | Crashlytics.recordError(e, stack, reason: "Contact sync api error"); 124 | syncContactTrace.putAttribute('error', e); 125 | syncContactTrace.stop(); 126 | return; 127 | } 128 | Database db = await DB().getDb(); 129 | var dbUsers = (await db.query('user')).map((e) => User.fromMap(e)).toList(); 130 | var contactDiff = _getContactDiff(dbUsers, users); 131 | 132 | Batch batch = db.batch(); 133 | contactDiff[0].forEach((user) { 134 | batch.rawInsert("""INSERT OR REPLACE INTO user ( 135 | username, 136 | name, 137 | pic, 138 | hasAccount, 139 | status 140 | ) values(?,?,?,?,?);""", user.toMap(persistent: true).values.toList()); 141 | }); 142 | contactDiff[1].forEach((user) { 143 | batch.rawUpdate("""UPDATE user SET name=?, 144 | pic=?, 145 | hasAccount=?, 146 | status=? 147 | WHERE username=?; 148 | """, [ 149 | user.name, 150 | user.pic, 151 | user.hasAccount ? 1 : 0, 152 | enumToInt(user.status, UserStatus.values), 153 | user.username 154 | ]); 155 | if (user.username != currentUser.username) { 156 | batch.rawUpdate("""UPDATE chat SET title=?, pic=? 157 | WHERE type=1 and id in ( 158 | SELECT chatid FROM chat_user 159 | WHERE userid=? 160 | ); 161 | """, [user.name, user.pic, user.username]); 162 | } 163 | }); 164 | contactDiff[2].forEach((user) { 165 | batch.rawUpdate("""UPDATE user SET name=?, 166 | pic=?, 167 | hasAccount=?, 168 | status=? 169 | WHERE username=?; 170 | """, [ 171 | user.username, // Set name as username or phone number for deleted user 172 | user.pic, 173 | user.hasAccount ? 1 : 0, 174 | enumToInt(UserStatus.DELETED, UserStatus.values), 175 | user.username 176 | ]); 177 | if (user.username != currentUser.username) { 178 | batch.rawUpdate("""UPDATE chat SET title=?, pic=? 179 | WHERE type=1 and id in ( 180 | SELECT chatid FROM chat_user 181 | WHERE userid=? 182 | ); 183 | """, [user.username, user.pic, user.username]); 184 | } 185 | }); 186 | await batch.commit(); 187 | syncContactTrace.stop(); 188 | _syncInProgress = false; 189 | } 190 | 191 | static Future> _getContacts() async { 192 | final permission = await Permission.contacts.status; 193 | if (!permission.isGranted) return []; 194 | Iterable contacts = await FlutterContacts.getContacts( 195 | withProperties: true, withThumbnail: false, withPhoto: false); 196 | List users = []; 197 | contacts.forEach((contact) { 198 | (contact.phones).forEach((phone) { 199 | String? phoneNumber = normalizePhoneNumber(phone.normalizedNumber); 200 | if (phoneNumber != null) { 201 | users.add(User( 202 | contact.displayName.isNotEmpty 203 | ? contact.displayName 204 | : phoneNumber, 205 | phoneNumber, 206 | null)); 207 | } 208 | }); 209 | }); 210 | return users; 211 | } 212 | 213 | static List> _getContactDiff( 214 | List dbUsers, List users) { 215 | List userToUpdate = []; 216 | List userToDelete = []; 217 | List userToInsert = []; 218 | userToDelete = dbUsers 219 | .where((u) => (u.status != UserStatus.UNKNOWN && !users.contains(u))) 220 | .toList(); 221 | users.forEach((u) { 222 | User? user = find(dbUsers, (e) => u == e); 223 | if (user == null) { 224 | userToInsert.add(u); 225 | } else if (user.name != u.name || 226 | user.pic != u.pic || 227 | user.hasAccount != u.hasAccount || 228 | user.status != u.status) { 229 | userToUpdate.add(u); 230 | } else { 231 | userToInsert.add(u); 232 | } 233 | }); 234 | return [userToInsert, userToUpdate, userToDelete]; 235 | } 236 | } 237 | --------------------------------------------------------------------------------