commit b5f2ecd56fe0daf74ba05e20244f3ccc6ecb548b Author: Christian Pauly Date: Wed Jan 1 19:10:13 2020 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..29b579d --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +lib/generated_plugin_registrant.dart + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Exceptions to above rules. +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..361e1e4 --- /dev/null +++ b/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 18cd7a3601bcffb36fdf2f679f763b5e827c2e8e + channel: beta + +project_type: app diff --git a/README.md b/README.md new file mode 100644 index 0000000..183809f --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# fluffychat + +Chat with your friends. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..bc2100d --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,7 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..a7e3441 --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,67 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 28 + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "chat.fluffy.fluffychat" + minSdkVersion 16 + targetSdkVersion 28 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..fc480dc --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d5fc4f2 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/chat/fluffy/fluffychat/MainActivity.kt b/android/app/src/main/kotlin/chat/fluffy/fluffychat/MainActivity.kt new file mode 100644 index 0000000..97f1298 --- /dev/null +++ b/android/app/src/main/kotlin/chat/fluffy/fluffychat/MainActivity.kt @@ -0,0 +1,12 @@ +package chat.fluffy.fluffychat + +import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugins.GeneratedPluginRegistrant + +class MainActivity: FlutterActivity() { + override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { + GeneratedPluginRegistrant.registerWith(flutterEngine); + } +} diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/launcher_icon.png b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png new file mode 100644 index 0000000..635d0f0 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/launcher_icon.png b/android/app/src/main/res/mipmap-mdpi/launcher_icon.png new file mode 100644 index 0000000..314b7eb Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png new file mode 100644 index 0000000..c2c13d6 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png new file mode 100644 index 0000000..241c889 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png new file mode 100644 index 0000000..1fd24a1 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..00fa441 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..fc480dc --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..3100ad2 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.3.50' + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.5.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..38c8d45 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1536M +android.enableR8=true +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..296b146 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..5a2f14f --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,15 @@ +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} diff --git a/assets/fluffychat-banner.png b/assets/fluffychat-banner.png new file mode 100644 index 0000000..d85c897 Binary files /dev/null and b/assets/fluffychat-banner.png differ diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 0000000..bbe3ccb Binary files /dev/null and b/assets/logo.png differ diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..e96ef60 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,32 @@ +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..6b4c0f7 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 8.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..e8efba1 --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..399e934 --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..b30a428 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,90 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def parse_KV_file(file, separator='=') + file_abs_path = File.expand_path(file) + if !File.exists? file_abs_path + return []; + end + generated_key_values = {} + skip_line_start_symbols = ["#", "/"] + File.foreach(file_abs_path) do |line| + next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } + plugin = line.split(pattern=separator) + if plugin.length == 2 + podname = plugin[0].strip() + path = plugin[1].strip() + podpath = File.expand_path("#{path}", file_abs_path) + generated_key_values[podname] = podpath + else + puts "Invalid plugin specification: #{line}" + end + end + generated_key_values +end + +target 'Runner' do + use_frameworks! + use_modular_headers! + + # Flutter Pod + + copied_flutter_dir = File.join(__dir__, 'Flutter') + copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework') + copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec') + unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path) + # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet. + # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration. + # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist. + + generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig') + unless File.exist?(generated_xcode_build_settings_path) + raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path) + cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR']; + + unless File.exist?(copied_framework_path) + FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir) + end + unless File.exist?(copied_podspec_path) + FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir) + end + end + + # Keep pod path relative so it can be checked into Podfile.lock. + pod 'Flutter', :path => 'Flutter' + + # Plugin Pods + + # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock + # referring to absolute paths on developers' machines. + system('rm -rf .symlinks') + system('mkdir -p .symlinks/plugins') + plugin_pods = parse_KV_file('../.flutter-plugins') + plugin_pods.each do |name, path| + symlink = File.join('.symlinks', 'plugins', name) + File.symlink(path, symlink) + pod name, :path => File.join(symlink, 'ios') + end +end + +# Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system. +install! 'cocoapods', :disable_input_output_paths => true + +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['ENABLE_BITCODE'] = 'NO' + end + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..2ba109a --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,67 @@ +PODS: + - file_picker (0.0.1): + - Flutter + - Flutter (1.0.0) + - FMDB (2.7.5): + - FMDB/standard (= 2.7.5) + - FMDB/standard (2.7.5) + - image_picker (0.0.1): + - Flutter + - path_provider (0.0.1): + - Flutter + - sqflite (0.0.1): + - Flutter + - FMDB (~> 2.7.2) + - url_launcher (0.0.1): + - Flutter + - url_launcher_macos (0.0.1): + - Flutter + - url_launcher_web (0.0.1): + - Flutter + +DEPENDENCIES: + - file_picker (from `.symlinks/plugins/file_picker/ios`) + - Flutter (from `Flutter`) + - image_picker (from `.symlinks/plugins/image_picker/ios`) + - path_provider (from `.symlinks/plugins/path_provider/ios`) + - sqflite (from `.symlinks/plugins/sqflite/ios`) + - url_launcher (from `.symlinks/plugins/url_launcher/ios`) + - url_launcher_macos (from `.symlinks/plugins/url_launcher_macos/ios`) + - url_launcher_web (from `.symlinks/plugins/url_launcher_web/ios`) + +SPEC REPOS: + trunk: + - FMDB + +EXTERNAL SOURCES: + file_picker: + :path: ".symlinks/plugins/file_picker/ios" + Flutter: + :path: Flutter + image_picker: + :path: ".symlinks/plugins/image_picker/ios" + path_provider: + :path: ".symlinks/plugins/path_provider/ios" + sqflite: + :path: ".symlinks/plugins/sqflite/ios" + url_launcher: + :path: ".symlinks/plugins/url_launcher/ios" + url_launcher_macos: + :path: ".symlinks/plugins/url_launcher_macos/ios" + url_launcher_web: + :path: ".symlinks/plugins/url_launcher_web/ios" + +SPEC CHECKSUMS: + file_picker: 408623be2125b79a4539cf703be3d4b3abe5e245 + Flutter: 0e3d915762c693b495b44d77113d4970485de6ec + FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a + image_picker: e3eacd46b94694dde7cf2705955cece853aa1a8f + path_provider: fb74bd0465e96b594bb3b5088ee4a4e7bb1f2a9d + sqflite: 4001a31ff81d210346b500c55b17f4d6c7589dd0 + url_launcher: a1c0cc845906122c4784c542523d8cacbded5626 + url_launcher_macos: fd7894421cd39320dce5f292fc99ea9270b2a313 + url_launcher_web: e5527357f037c87560776e36436bf2b0288b965c + +PODFILE CHECKSUM: 1b66dae606f75376c5f2135a8290850eeb09ae83 + +COCOAPODS: 1.8.4 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..114d793 --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,584 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; + 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 825E8C4DDFD87D133ACCCC1A /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14FAF412639B88C5C2721D00 /* Pods_Runner.framework */; }; + 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; + 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, + 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0EF69A899CE7F4E07C5F6599 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 14FAF412639B88C5C2721D00 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; + 6D40C7CF338C6054DF530EE6 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B7CDE45F1181122C41ED566D /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, + 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, + 825E8C4DDFD87D133ACCCC1A /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 587F23226AD40CECE246E2A7 /* Pods */ = { + isa = PBXGroup; + children = ( + 0EF69A899CE7F4E07C5F6599 /* Pods-Runner.debug.xcconfig */, + 6D40C7CF338C6054DF530EE6 /* Pods-Runner.release.xcconfig */, + B7CDE45F1181122C41ED566D /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B80C3931E831B6300D905FE /* App.framework */, + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEBA1CF902C7004384FC /* Flutter.framework */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 587F23226AD40CECE246E2A7 /* Pods */, + 9BE4B8D2394D0E419F06E05E /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 9BE4B8D2394D0E419F06E05E /* Frameworks */ = { + isa = PBXGroup; + children = ( + 14FAF412639B88C5C2721D00 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + F4901FBEE4C8B0FA7B998652 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + BAF2985AB306D24B14B17E3F /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = "The Chromium Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + BAF2985AB306D24B14B17E3F /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + F4901FBEE4C8B0FA7B998652 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = chat.fluffy.fluffychat; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = chat.fluffy.fluffychat; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = chat.fluffy.fluffychat; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..a28140c --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..f6fe1f8 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..1dc7b40 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..24c081a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..8f59c63 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..b533ddb Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..ed3c09e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..9f14b9b Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..24c081a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..2f60058 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..3c51f40 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png new file mode 100644 index 0000000..0287cf6 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png new file mode 100644 index 0000000..61b1565 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png new file mode 100644 index 0000000..4466711 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png new file mode 100644 index 0000000..6aa1102 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..3c51f40 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..3ec7d37 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png new file mode 100644 index 0000000..635d0f0 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png new file mode 100644 index 0000000..241c889 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..8ad0e71 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..db98a7b Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..af4aeb8 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +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. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..6c475ed --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + fluffychat + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + NSPhotoLibraryUsageDescription + Share photos with your contacts + NSCameraUsageDescription + Share photos with your contacts + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..7335fdf --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" \ No newline at end of file diff --git a/lib/components/adaptive_page_layout.dart b/lib/components/adaptive_page_layout.dart new file mode 100644 index 0000000..70c4064 --- /dev/null +++ b/lib/components/adaptive_page_layout.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; + +enum FocusPage { FIRST, SECOND } + +class AdaptivePageLayout extends StatelessWidget { + final Widget firstScaffold; + final Widget secondScaffold; + final FocusPage primaryPage; + final double minWidth; + + static const double defaultMinWidth = 400; + static bool columnMode(BuildContext context) => + MediaQuery.of(context).size.width > 2 * defaultMinWidth; + + AdaptivePageLayout( + {this.firstScaffold, + this.secondScaffold, + this.primaryPage = FocusPage.FIRST, + this.minWidth = defaultMinWidth, + Key key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return OrientationBuilder(builder: (context, orientation) { + if (orientation == Orientation.portrait || + columnMode(context)) if (primaryPage == FocusPage.FIRST) + return firstScaffold; + else + return secondScaffold; + return Row( + children: [ + Container( + width: minWidth, + child: firstScaffold, + ), + Container( + width: 1, + color: Color(0xFFE8E8E8), + ), + Expanded( + child: Container( + child: secondScaffold, + ), + ) + ], + ); + }); + } +} diff --git a/lib/components/avatar.dart b/lib/components/avatar.dart new file mode 100644 index 0000000..1ca495b --- /dev/null +++ b/lib/components/avatar.dart @@ -0,0 +1,39 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'matrix.dart'; + +class Avatar extends StatelessWidget { + final MxContent mxContent; + final double size; + + const Avatar(this.mxContent, {this.size = 40, Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final String src = mxContent.getThumbnail( + Matrix.of(context).client, + width: size * MediaQuery.of(context).devicePixelRatio, + height: size * MediaQuery.of(context).devicePixelRatio, + method: ThumbnailMethod.scale, + ); + return CircleAvatar( + radius: size / 2, + backgroundImage: mxContent.mxc?.isNotEmpty ?? false + ? kIsWeb + ? NetworkImage( + src, + ) + : CachedNetworkImageProvider( + src, + ) + : null, + backgroundColor: Color(0xFFF8F8F8), + child: mxContent.mxc.isEmpty + ? Text("@", style: TextStyle(color: Colors.blueGrey)) + : null, + ); + } +} diff --git a/lib/components/chat_settings_popup_menu.dart b/lib/components/chat_settings_popup_menu.dart new file mode 100644 index 0000000..6b6cd4d --- /dev/null +++ b/lib/components/chat_settings_popup_menu.dart @@ -0,0 +1,70 @@ +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/utils/app_route.dart'; +import 'package:fluffychat/views/chat_details.dart'; +import 'package:fluffychat/views/chat_list.dart'; +import 'package:flutter/material.dart'; + +import 'matrix.dart'; + +class ChatSettingsPopupMenu extends StatelessWidget { + final Room room; + final bool displayChatDetails; + const ChatSettingsPopupMenu(this.room, this.displayChatDetails, {Key key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + List> items = >[ + room.pushRuleState == PushRuleState.notify + ? const PopupMenuItem( + value: "mute", + child: Text('Mute chat'), + ) + : const PopupMenuItem( + value: "unmute", + child: Text('Unmute chat'), + ), + const PopupMenuItem( + value: "leave", + child: Text('Leave'), + ), + ]; + if (displayChatDetails) + items.insert( + 0, + const PopupMenuItem( + value: "details", + child: Text('Chat details'), + ), + ); + return PopupMenuButton( + onSelected: (String choice) async { + switch (choice) { + case "leave": + await Matrix.of(context).tryRequestWithLoadingDialog(room.leave()); + Navigator.of(context).pushAndRemoveUntil( + AppRoute.defaultRoute(context, ChatListView()), + (Route r) => false); + break; + case "mute": + await Matrix.of(context).tryRequestWithLoadingDialog( + room.setPushRuleState(PushRuleState.mentions_only)); + break; + case "unmute": + await Matrix.of(context).tryRequestWithLoadingDialog( + room.setPushRuleState(PushRuleState.notify)); + break; + case "details": + Navigator.of(context).push( + AppRoute.defaultRoute( + context, + ChatDetails(room), + ), + ); + break; + } + }, + itemBuilder: (BuildContext context) => items, + ); + } +} diff --git a/lib/components/content_banner.dart b/lib/components/content_banner.dart new file mode 100644 index 0000000..4480489 --- /dev/null +++ b/lib/components/content_banner.dart @@ -0,0 +1,55 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'matrix.dart'; + +class ContentBanner extends StatelessWidget { + final MxContent mxContent; + final double height; + final IconData defaultIcon; + final bool loading; + + const ContentBanner(this.mxContent, + {this.height = 400, + this.defaultIcon = Icons.people_outline, + this.loading = false, + Key key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + final mediaQuery = MediaQuery.of(context); + final int bannerSize = + (mediaQuery.size.width * mediaQuery.devicePixelRatio).toInt(); + final String src = mxContent.getThumbnail( + Matrix.of(context).client, + width: bannerSize, + height: bannerSize, + method: ThumbnailMethod.scale, + ); + return Container( + height: 200, + color: Color(0xFFF8F8F8), + child: !loading + ? mxContent.mxc?.isNotEmpty ?? false + ? kIsWeb + ? Image.network( + src, + height: 200, + fit: BoxFit.cover, + ) + : CachedNetworkImage( + imageUrl: src, + height: 200, + fit: BoxFit.cover, + placeholder: (c, s) => + Center(child: CircularProgressIndicator()), + errorWidget: (c, s, o) => Icon(Icons.error, size: 200), + ) + : Icon(defaultIcon, size: 200) + : Icon(defaultIcon, size: 200), + ); + } +} diff --git a/lib/components/dialogs/new_group_dialog.dart b/lib/components/dialogs/new_group_dialog.dart new file mode 100644 index 0000000..87059de --- /dev/null +++ b/lib/components/dialogs/new_group_dialog.dart @@ -0,0 +1,59 @@ +import 'package:fluffychat/views/chat.dart'; +import 'package:flutter/material.dart'; + +import '../matrix.dart'; + +class NewGroupDialog extends StatelessWidget { + final TextEditingController controller = TextEditingController(); + + void submitAction(BuildContext context) async { + final MatrixState matrix = Matrix.of(context); + Map params = {}; + if (controller.text.isNotEmpty) params["name"] = controller.text; + final String roomID = await matrix.tryRequestWithLoadingDialog( + matrix.client.createRoom(params: params), + ); + Navigator.of(context).pop(); + if (roomID != null) + Navigator.push( + context, + MaterialPageRoute(builder: (context) => Chat(roomID)), + ); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text("Create new group"), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: controller, + autofocus: true, + autocorrect: false, + textInputAction: TextInputAction.go, + onSubmitted: (s) => submitAction(context), + decoration: InputDecoration( + labelText: "Group name", + icon: Icon(Icons.people), + hintText: "Enter a group name"), + ), + ], + ), + actions: [ + FlatButton( + child: Text("Close".toUpperCase(), + style: TextStyle(color: Colors.blueGrey)), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + FlatButton( + child: Text("Create".toUpperCase()), + onPressed: () => submitAction(context), + ), + ], + ); + } +} diff --git a/lib/components/dialogs/new_private_chat_dialog.dart b/lib/components/dialogs/new_private_chat_dialog.dart new file mode 100644 index 0000000..977ffda --- /dev/null +++ b/lib/components/dialogs/new_private_chat_dialog.dart @@ -0,0 +1,72 @@ +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/views/chat.dart'; +import 'package:flutter/material.dart'; + +import '../matrix.dart'; + +class NewPrivateChatDialog extends StatelessWidget { + final TextEditingController controller = TextEditingController(); + + void submitAction(BuildContext context) async { + if (controller.text.isEmpty) return; + final MatrixState matrix = Matrix.of(context); + final User user = User( + "@" + controller.text, + room: Room(id: "", client: matrix.client), + ); + final String roomID = + await matrix.tryRequestWithLoadingDialog(user.startDirectChat()); + Navigator.of(context).pop(); + + if (roomID != null) + Navigator.push( + context, + MaterialPageRoute(builder: (context) => Chat(roomID)), + ); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text("New private chat"), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: controller, + autofocus: true, + autocorrect: false, + textInputAction: TextInputAction.go, + onSubmitted: (s) => submitAction(context), + decoration: InputDecoration( + labelText: "Enter a username", + icon: Icon(Icons.account_circle), + prefixText: "@", + hintText: "username:homeserver"), + ), + SizedBox(height: 16), + Text( + "Your username is ${Matrix.of(context).client.userID}", + style: TextStyle( + color: Colors.blueGrey, + fontSize: 12, + ), + ), + ], + ), + actions: [ + FlatButton( + child: Text("Close".toUpperCase(), + style: TextStyle(color: Colors.blueGrey)), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + FlatButton( + child: Text("Continue".toUpperCase()), + onPressed: () => submitAction(context), + ), + ], + ); + } +} diff --git a/lib/components/dialogs/redact_message_dialog.dart b/lib/components/dialogs/redact_message_dialog.dart new file mode 100644 index 0000000..c1c5ee0 --- /dev/null +++ b/lib/components/dialogs/redact_message_dialog.dart @@ -0,0 +1,32 @@ +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:flutter/material.dart'; + +import '../matrix.dart'; + +class RedactMessageDialog extends StatelessWidget { + final Event event; + const RedactMessageDialog(this.event); + + void removeAction(BuildContext context) { + Matrix.of(context).tryRequestWithLoadingDialog(event.redact()); + Navigator.of(context).pop(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text("Message will be removed for all participants"), + actions: [ + FlatButton( + child: Text("Close".toUpperCase(), + style: TextStyle(color: Colors.blueGrey)), + onPressed: () => Navigator.of(context).pop(), + ), + FlatButton( + child: Text("Remove".toUpperCase()), + onPressed: () => removeAction(context), + ), + ], + ); + } +} diff --git a/lib/components/list_items/chat_list_item.dart b/lib/components/list_items/chat_list_item.dart new file mode 100644 index 0000000..b1c3fb9 --- /dev/null +++ b/lib/components/list_items/chat_list_item.dart @@ -0,0 +1,67 @@ +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/components/message_content.dart'; +import 'package:fluffychat/utils/app_route.dart'; +import 'package:fluffychat/views/chat.dart'; +import 'package:flutter/material.dart'; + +import '../avatar.dart'; + +class ChatListItem extends StatelessWidget { + final Room room; + final bool activeChat; + + const ChatListItem(this.room, {this.activeChat = false}); + + @override + Widget build(BuildContext context) { + return Material( + color: activeChat ? Color(0xFFE8E8E8) : Colors.white, + child: ListTile( + leading: Avatar(room.avatar), + title: Text(room.displayname), + subtitle: MessageContent(room.lastEvent, textOnly: true), + onTap: () { + if (activeChat) + Navigator.pushReplacement( + context, + AppRoute.defaultRoute(context, Chat(room.id)), + ); + else + Navigator.push( + context, + AppRoute.defaultRoute(context, Chat(room.id)), + ); + }, + onLongPress: () {}, + trailing: Container( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text(room.timeCreated.toEventTimeString()), + room.notificationCount > 0 + ? Container( + width: 20, + height: 20, + margin: EdgeInsets.only(top: 3), + decoration: BoxDecoration( + color: room.highlightCount > 0 + ? Colors.red + : Color(0xFF5625BA), + borderRadius: BorderRadius.circular(20), + ), + child: Center( + child: Text( + room.notificationCount.toString(), + style: TextStyle(color: Colors.white), + ), + ), + ) + : Text(" "), + ], + ), + ), + ), + ); + } +} diff --git a/lib/components/list_items/message.dart b/lib/components/list_items/message.dart new file mode 100644 index 0000000..f060b07 --- /dev/null +++ b/lib/components/list_items/message.dart @@ -0,0 +1,128 @@ +import 'package:bubble/bubble.dart'; +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/components/dialogs/redact_message_dialog.dart'; +import 'package:fluffychat/components/message_content.dart'; +import 'package:flutter/material.dart'; + +import '../avatar.dart'; +import '../matrix.dart'; +import 'state_message.dart'; + +class Message extends StatelessWidget { + final Event event; + + const Message(this.event); + + @override + Widget build(BuildContext context) { + if (event.typeKey != "m.room.message") return StateMessage(event); + + Client client = Matrix.of(context).client; + final bool ownMessage = event.senderId == client.userID; + Alignment alignment = ownMessage ? Alignment.topRight : Alignment.topLeft; + Color color = Color(0xFFF8F8F8); + BubbleNip nip = ownMessage ? BubbleNip.rightBottom : BubbleNip.leftBottom; + final Color textColor = ownMessage ? Colors.white : Colors.black; + MainAxisAlignment rowMainAxisAlignment = + ownMessage ? MainAxisAlignment.end : MainAxisAlignment.start; + + if (ownMessage) { + color = event.status == -1 ? Colors.redAccent : Color(0xFF5625BA); + } + List> popupMenuList = []; + if (event.canRedact && !event.redacted && event.status > 1) + popupMenuList.add( + const PopupMenuItem( + value: "remove", + child: Text('Remove message'), + ), + ); + if (ownMessage && event.status == -1) { + popupMenuList.add( + const PopupMenuItem( + value: "resend", + child: Text('Send again'), + ), + ); + popupMenuList.add( + const PopupMenuItem( + value: "delete", + child: Text('Delete message'), + ), + ); + } + + List rowChildren = [ + Expanded( + child: PopupMenuButton( + onSelected: (String choice) async { + switch (choice) { + case "remove": + showDialog( + context: context, + builder: (BuildContext context) => RedactMessageDialog(event), + ); + break; + case "resend": + event.sendAgain(); + break; + case "delete": + event.remove(); + break; + } + }, + itemBuilder: (BuildContext context) => popupMenuList, + child: Opacity( + opacity: event.status == 0 ? 0.5 : 1, + child: Bubble( + elevation: 0, + alignment: alignment, + margin: BubbleEdges.symmetric(horizontal: 4), + color: color, + nip: nip, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + ownMessage ? "You" : event.sender.calcDisplayname(), + style: TextStyle( + color: textColor, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(width: 4), + Text( + event.time.toEventTimeString(), + style: TextStyle(color: textColor, fontSize: 12), + ), + ], + ), + MessageContent( + event, + textColor: textColor, + ), + ], + ), + ), + ), + ), + ), + ]; + if (ownMessage) + rowChildren.add(Avatar(event.sender.avatarUrl)); + else + rowChildren.insert(0, Avatar(event.sender.avatarUrl)); + return Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: rowMainAxisAlignment, + children: rowChildren, + ), + ); + } +} diff --git a/lib/components/list_items/participant_list_item.dart b/lib/components/list_items/participant_list_item.dart new file mode 100644 index 0000000..692e34d --- /dev/null +++ b/lib/components/list_items/participant_list_item.dart @@ -0,0 +1,119 @@ +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/utils/app_route.dart'; +import 'package:fluffychat/views/chat.dart'; +import 'package:flutter/material.dart'; + +import '../avatar.dart'; +import '../matrix.dart'; + +class ParticipantListItem extends StatelessWidget { + final User user; + + const ParticipantListItem(this.user); + + participantAction(BuildContext context, String action) async { + final MatrixState matrix = Matrix.of(context); + switch (action) { + case "ban": + matrix.tryRequestWithLoadingDialog(user.ban()); + break; + case "unban": + matrix.tryRequestWithLoadingDialog(user.unban()); + break; + case "kick": + matrix.tryRequestWithLoadingDialog(user.kick()); + break; + case "admin": + matrix.tryRequestWithLoadingDialog(user.setPower(100)); + break; + case "user": + matrix.tryRequestWithLoadingDialog(user.setPower(100)); + break; + case "message": + final String roomId = await user.startDirectChat(); + Navigator.of(context).pushAndRemoveUntil( + AppRoute.defaultRoute( + context, + Chat(roomId), + ), + (Route r) => r.isFirst); + break; + } + } + + @override + Widget build(BuildContext context) { + const Map membershipBatch = { + Membership.join: "", + Membership.ban: "Banned", + Membership.invite: "Invited", + Membership.leave: "Left", + }; + final String permissionBatch = user.powerLevel == 100 + ? "Admin" + : user.powerLevel >= 50 ? "Moderator" : ""; + List> items = >[]; + if (user.canChangePowerLevel && + user.room.ownPowerLevel == 100 && + user.powerLevel != 100) + items.add( + PopupMenuItem(child: Text("Make an admin"), value: "admin"), + ); + if (user.canChangePowerLevel && user.powerLevel != 0) + items.add( + PopupMenuItem(child: Text("Revoke all permissions"), value: "user"), + ); + if (user.canKick) + items.add( + PopupMenuItem(child: Text("Kick from group"), value: "kick"), + ); + if (user.canBan && user.membership != Membership.ban) + items.add( + PopupMenuItem(child: Text("Ban from group"), value: "ban"), + ); + else if (user.canBan && user.membership == Membership.ban) + items.add( + PopupMenuItem(child: Text("Remove exile"), value: "unban"), + ); + if (user.id != Matrix.of(context).client.userID) + items.add( + PopupMenuItem(child: Text("Send a message"), value: "message"), + ); + return PopupMenuButton( + onSelected: (action) => participantAction(context, action), + itemBuilder: (c) => items, + child: ListTile( + title: Row( + children: [ + Text(user.calcDisplayname()), + permissionBatch.isEmpty + ? Container() + : Container( + padding: EdgeInsets.all(4), + margin: EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: Color(0xFFF8F8F8), + borderRadius: BorderRadius.circular(8), + ), + child: Center(child: Text(permissionBatch)), + ), + membershipBatch[user.membership].isEmpty + ? Container() + : Container( + padding: EdgeInsets.all(4), + margin: EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: Color(0xFFF8F8F8), + borderRadius: BorderRadius.circular(8), + ), + child: + Center(child: Text(membershipBatch[user.membership])), + ), + ], + ), + subtitle: Text(user.id), + leading: Avatar(user.avatarUrl), + ), + ); + } +} diff --git a/lib/components/list_items/state_message.dart b/lib/components/list_items/state_message.dart new file mode 100644 index 0000000..15c9341 --- /dev/null +++ b/lib/components/list_items/state_message.dart @@ -0,0 +1,23 @@ +import 'package:bubble/bubble.dart'; +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:flutter/material.dart'; + +import '../message_content.dart'; + +class StateMessage extends StatelessWidget { + final Event event; + const StateMessage(this.event); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Bubble( + color: Color(0xFFF8F8F8), + elevation: 0, + alignment: Alignment.center, + child: MessageContent(event), + ), + ); + } +} diff --git a/lib/components/matrix.dart b/lib/components/matrix.dart new file mode 100644 index 0000000..f81c29b --- /dev/null +++ b/lib/components/matrix.dart @@ -0,0 +1,167 @@ +import 'dart:convert'; +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/utils/sqflite_store.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:localstorage/localstorage.dart'; +import 'package:toast/toast.dart'; + +class Matrix extends StatefulWidget { + final Widget child; + + final String clientName; + + final Client client; + + Matrix({this.child, this.clientName, this.client, Key key}) : super(key: key); + + @override + MatrixState createState() => MatrixState(); + + /// Returns the (nearest) Client instance of your application. + static MatrixState of(BuildContext context) { + MatrixState newState = + (context.dependOnInheritedWidgetOfExactType<_InheritedMatrix>()).data; + newState.context = context; + return newState; + } +} + +class MatrixState extends State { + Client client; + BuildContext context; + + /// Used to load the old account if there is no store available. + void loadAccount() async { + final LocalStorage storage = LocalStorage('LocalStorage'); + await storage.ready; + + final credentialsStr = storage.getItem(widget.clientName); + if (credentialsStr == null || credentialsStr.isEmpty) { + client.connection.onLoginStateChanged.add(LoginState.loggedOut); + return; + } + print("[Matrix] Restoring account credentials"); + final Map credentials = json.decode(credentialsStr); + client.connection.connect( + newDeviceID: credentials["deviceID"], + newDeviceName: credentials["deviceName"], + newHomeserver: credentials["homeserver"], + newLazyLoadMembers: credentials["lazyLoadMembers"], + //newMatrixVersions: credentials["matrixVersions"], // FIXME: wrong List type + newToken: credentials["token"], + newUserID: credentials["userID"], + ); + } + + /// Used to save the current account persistently if there is no store available. + Future saveAccount() async { + if (!kIsWeb) return; + print("[Matrix] Save account credentials in crypted preferences"); + final Map credentials = { + "deviceID": client.deviceID, + "deviceName": client.deviceName, + "homeserver": client.homeserver, + "lazyLoadMembers": client.lazyLoadMembers, + "matrixVersions": client.matrixVersions, + "token": client.accessToken, + "userID": client.userID, + }; + + final LocalStorage storage = LocalStorage('LocalStorage'); + await storage.ready; + await storage.setItem(widget.clientName, json.encode(credentials)); + return; + } + + void clean() async { + if (!kIsWeb) return; + print("Clear session..."); + + final LocalStorage storage = LocalStorage('LocalStorage'); + await storage.ready; + storage.deleteItem(widget.clientName); + } + + BuildContext _loadingDialogContext; + + Future tryRequestWithLoadingDialog(Future request) async { + showLoadingDialog(context); + final dynamic = await tryRequestWithErrorToast(request); + hideLoadingDialog(); + return dynamic; + } + + Future tryRequestWithErrorToast(Future request) async { + try { + return await request; + } catch (exception) { + Toast.show( + exception.toString(), + context, + duration: Toast.LENGTH_LONG, + ); + return false; + } + } + + showLoadingDialog(BuildContext context) { + _loadingDialogContext = context; + showDialog( + context: _loadingDialogContext, + barrierDismissible: false, + builder: (BuildContext context) => AlertDialog( + content: Row( + children: [ + CircularProgressIndicator(), + SizedBox(width: 16), + Text("Loading... Please wait"), + ], + ), + ), + ); + } + + hideLoadingDialog() => Navigator.of(_loadingDialogContext)?.pop(); + + @override + void initState() { + if (widget.client == null) { + client = Client(widget.clientName, debug: true); + if (!kIsWeb) + client.store = Store(client); + else + loadAccount(); + } else { + client = widget.client; + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + return _InheritedMatrix( + data: this, + child: widget.child, + ); + } +} + +class _InheritedMatrix extends InheritedWidget { + final MatrixState data; + + _InheritedMatrix({Key key, this.data, Widget child}) + : super(key: key, child: child); + + @override + bool updateShouldNotify(_InheritedMatrix old) { + bool update = old.data.client.accessToken != this.data.client.accessToken || + old.data.client.userID != this.data.client.userID || + old.data.client.matrixVersions != this.data.client.matrixVersions || + old.data.client.lazyLoadMembers != this.data.client.lazyLoadMembers || + old.data.client.deviceID != this.data.client.deviceID || + old.data.client.deviceName != this.data.client.deviceName || + old.data.client.homeserver != this.data.client.homeserver; + return update; + } +} diff --git a/lib/components/message_content.dart b/lib/components/message_content.dart new file mode 100644 index 0000000..2769293 --- /dev/null +++ b/lib/components/message_content.dart @@ -0,0 +1,78 @@ +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import 'matrix.dart'; + +class MessageContent extends StatelessWidget { + final Event event; + final Color textColor; + final bool textOnly; + + const MessageContent(this.event, {this.textColor, this.textOnly = false}); + + @override + Widget build(BuildContext context) { + final int maxLines = textOnly ? 1 : null; + if (textOnly) + return Text( + event.getBody(), + style: TextStyle( + color: textColor, + decoration: event.redacted ? TextDecoration.lineThrough : null, + ), + maxLines: maxLines, + ); + switch (event.type) { + case EventTypes.Audio: + case EventTypes.Image: + case EventTypes.File: + case EventTypes.Video: + return Container( + width: 200, + child: RaisedButton( + color: Colors.blueGrey, + child: Text( + "Download ${event.getBody()}", + overflow: TextOverflow.fade, + softWrap: false, + maxLines: 1, + ), + onPressed: () => launch( + MxContent(event.content["url"]) + .getDownloadLink(Matrix.of(context).client), + ), + ), + ); + case EventTypes.Text: + case EventTypes.Reply: + case EventTypes.Notice: + return Text( + event.getBody(), + style: TextStyle( + color: textColor, + decoration: event.redacted ? TextDecoration.lineThrough : null, + ), + ); + case EventTypes.Emote: + return Text( + "* " + event.getBody(), + maxLines: maxLines, + style: TextStyle( + color: textColor, + fontStyle: FontStyle.italic, + decoration: event.redacted ? TextDecoration.lineThrough : null, + ), + ); + default: + return Text( + "${event.sender.calcDisplayname()} sent a ${event.typeKey} event", + maxLines: maxLines, + style: TextStyle( + color: textColor, + decoration: event.redacted ? TextDecoration.lineThrough : null, + ), + ); + } + } +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..a3af3b6 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,59 @@ +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:flutter/material.dart'; + +import 'components/matrix.dart'; +import 'views/chat_list.dart'; +import 'views/login.dart'; + +void main() => runApp(MyApp()); + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Matrix( + clientName: "FluffyWeb", + child: MaterialApp( + title: 'FluffyWeb', + theme: ThemeData( + primaryColor: Color(0xFF5625BA), + backgroundColor: Colors.white, + scaffoldBackgroundColor: Colors.white, + dialogTheme: DialogTheme( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + popupMenuTheme: PopupMenuThemeData( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + appBarTheme: AppBarTheme( + color: Colors.white, + elevation: 1, + textTheme: TextTheme( + title: TextStyle(color: Colors.black), + ), + iconTheme: IconThemeData(color: Colors.black), + ), + ), + home: Builder( + builder: (BuildContext context) => StreamBuilder( + stream: + Matrix.of(context).client.connection.onLoginStateChanged.stream, + builder: (context, snapshot) { + if (!snapshot.hasData) + return Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); + if (Matrix.of(context).client.isLogged()) return ChatListView(); + return LoginPage(); + }, + ), + ), + ), + ); + } +} diff --git a/lib/utils/app_route.dart b/lib/utils/app_route.dart new file mode 100644 index 0000000..c9f25b8 --- /dev/null +++ b/lib/utils/app_route.dart @@ -0,0 +1,34 @@ +import 'package:fluffychat/components/adaptive_page_layout.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class AppRoute extends PageRouteBuilder { + static Route defaultRoute(BuildContext context, Widget page) { + return context != null && !AdaptivePageLayout.columnMode(context) + ? CupertinoPageRoute( + builder: (BuildContext context) => page, + ) + : AppRoute(page); + } + + final Widget page; + AppRoute(this.page) + : super( + pageBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) => + page, + transitionsBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) => + FadeTransition( + opacity: animation, + child: child, + ), + ); +} diff --git a/lib/utils/sqflite_store.dart b/lib/utils/sqflite_store.dart new file mode 100644 index 0000000..a9ee2a7 --- /dev/null +++ b/lib/utils/sqflite_store.dart @@ -0,0 +1,588 @@ +/* + * Copyright (c) 2019 Zender & Kurtz GbR. + * + * Authors: + * Christian Pauly + * Marcel Radzio + * + * This file is part of famedlysdk_store_sqflite. + * + * famedlysdk_store_sqflite is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * famedlysdk_store_sqflite is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with famedlysdk_store_sqflite. If not, see . + */ + +library famedlysdk_store_sqflite; + +import 'dart:async'; +import 'dart:convert'; +import 'dart:core'; + +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:path/path.dart' as p; +import 'package:sqflite/sqflite.dart'; + +/// Responsible to store all data persistent and to query objects from the +/// database. +class Store extends StoreAPI { + final Client client; + + Store(this.client) { + _init(); + } + + Database _db; + + /// SQLite database for all persistent data. It is recommended to extend this + /// SDK instead of writing direct queries to the database. + //Database get db => _db; + + _init() async { + var databasePath = await getDatabasesPath(); + String path = p.join(databasePath, "FluffyMatrix.db"); + _db = await openDatabase(path, version: 14, + onCreate: (Database db, int version) async { + await createTables(db); + }, onUpgrade: (Database db, int oldVersion, int newVersion) async { + if (client.debug) + print( + "[Store] Migrate databse from version $oldVersion to $newVersion"); + if (oldVersion != newVersion) { + await schemes.forEach((String name, String scheme) async { + if (name != "Clients") await db.execute("DROP TABLE IF EXISTS $name"); + }); + await createTables(db); + await db.rawUpdate("UPDATE Clients SET prev_batch='' WHERE client=?", + [client.clientName]); + } + }); + + await _db.rawUpdate("UPDATE Events SET status=-1 WHERE status=0"); + + List list = await _db + .rawQuery("SELECT * FROM Clients WHERE client=?", [client.clientName]); + if (list.length == 1) { + var clientList = list[0]; + client.connection.connect( + newToken: clientList["token"], + newHomeserver: clientList["homeserver"], + newUserID: clientList["matrix_id"], + newDeviceID: clientList["device_id"], + newDeviceName: clientList["device_name"], + newLazyLoadMembers: clientList["lazy_load_members"] == 1, + newMatrixVersions: clientList["matrix_versions"].toString().split(","), + newPrevBatch: clientList["prev_batch"].toString().isEmpty + ? null + : clientList["prev_batch"], + ); + if (client.debug) + print("[Store] Restore client credentials of ${client.userID}"); + } else + client.connection.onLoginStateChanged.add(LoginState.loggedOut); + } + + Future createTables(Database db) async { + await schemes.forEach((String name, String scheme) async { + await db.execute(scheme); + }); + } + + Future queryPrevBatch() async { + List list = await txn.rawQuery( + "SELECT prev_batch FROM Clients WHERE client=?", [client.clientName]); + return list[0]["prev_batch"]; + } + + /// Will be automatically called when the client is logged in successfully. + Future storeClient() async { + await _db + .rawInsert('INSERT OR IGNORE INTO Clients VALUES(?,?,?,?,?,?,?,?,?)', [ + client.clientName, + client.accessToken, + client.homeserver, + client.userID, + client.deviceID, + client.deviceName, + client.prevBatch, + client.matrixVersions.join(","), + client.lazyLoadMembers, + ]); + return; + } + + /// Clears all tables from the database. + Future clear() async { + await _db + .rawDelete("DELETE FROM Clients WHERE client=?", [client.clientName]); + await schemes.forEach((String name, String scheme) async { + if (name != "Clients") await _db.rawDelete("DELETE FROM $name"); + }); + return; + } + + Future transaction(Future queries()) async { + return _db.transaction((txnObj) async { + txn = txnObj; + await queries(); + }); + } + + /// Will be automatically called on every synchronisation. Must be called inside of + // /// [transaction]. + Future storePrevBatch(dynamic sync) { + txn.rawUpdate("UPDATE Clients SET prev_batch=? WHERE client=?", + [client.prevBatch, client.clientName]); + return null; + } + + Future storeRoomPrevBatch(Room room) async { + await _db.rawUpdate("UPDATE Rooms SET prev_batch=? WHERE room_id=?", + [room.prev_batch, room.id]); + return null; + } + + /// Stores a RoomUpdate object in the database. Must be called inside of + /// [transaction]. + Future storeRoomUpdate(RoomUpdate roomUpdate) { + if (txn == null) return null; + // Insert the chat into the database if not exists + if (roomUpdate.membership != Membership.leave) + txn.rawInsert( + "INSERT OR IGNORE INTO Rooms " + "VALUES(?, ?, 0, 0, '', 0, 0, '') ", + [roomUpdate.id, roomUpdate.membership.toString().split('.').last]); + else { + txn.rawDelete("DELETE FROM Rooms WHERE room_id=? ", [roomUpdate.id]); + return null; + } + + // Update the notification counts and the limited timeline boolean and the summary + String updateQuery = + "UPDATE Rooms SET highlight_count=?, notification_count=?, membership=?"; + List updateArgs = [ + roomUpdate.highlight_count, + roomUpdate.notification_count, + roomUpdate.membership.toString().split('.').last + ]; + if (roomUpdate.summary?.mJoinedMemberCount != null) { + updateQuery += ", joined_member_count=?"; + updateArgs.add(roomUpdate.summary.mJoinedMemberCount); + } + if (roomUpdate.summary?.mInvitedMemberCount != null) { + updateQuery += ", invited_member_count=?"; + updateArgs.add(roomUpdate.summary.mInvitedMemberCount); + } + if (roomUpdate.summary?.mHeroes != null) { + updateQuery += ", heroes=?"; + updateArgs.add(roomUpdate.summary.mHeroes.join(",")); + } + updateQuery += " WHERE room_id=?"; + updateArgs.add(roomUpdate.id); + txn.rawUpdate(updateQuery, updateArgs); + + // Is the timeline limited? Then all previous messages should be + // removed from the database! + if (roomUpdate.limitedTimeline) { + txn.rawDelete("DELETE FROM Events WHERE room_id=?", [roomUpdate.id]); + txn.rawUpdate("UPDATE Rooms SET prev_batch=? WHERE room_id=?", + [roomUpdate.prev_batch, roomUpdate.id]); + } + return null; + } + + /// Stores an UserUpdate object in the database. Must be called inside of + /// [transaction]. + Future storeUserEventUpdate(UserUpdate userUpdate) { + if (txn == null) return null; + if (userUpdate.type == "account_data") + txn.rawInsert("INSERT OR REPLACE INTO AccountData VALUES(?, ?)", [ + userUpdate.eventType, + json.encode(userUpdate.content["content"]), + ]); + else if (userUpdate.type == "presence") + txn.rawInsert("INSERT OR REPLACE INTO Presences VALUES(?, ?, ?)", [ + userUpdate.eventType, + userUpdate.content["sender"], + json.encode(userUpdate.content["content"]), + ]); + return null; + } + + Future redactMessage(EventUpdate eventUpdate) async { + List> res = await _db.rawQuery( + "SELECT * FROM Events WHERE event_id=?", + [eventUpdate.content["redacts"]]); + if (res.length == 1) { + Event event = Event.fromJson(res[0], null); + event.setRedactionEvent(Event.fromJson(eventUpdate.content, null)); + final int changes1 = await _db.rawUpdate( + "UPDATE Events SET unsigned=?, content=?, prev_content=? WHERE event_id=?", + [ + json.encode(event.unsigned ?? ""), + json.encode(event.content ?? ""), + json.encode(event.prevContent ?? ""), + event.eventId, + ], + ); + final int changes2 = await _db.rawUpdate( + "UPDATE RoomStates SET unsigned=?, content=?, prev_content=? WHERE event_id=?", + [ + json.encode(event.unsigned ?? ""), + json.encode(event.content ?? ""), + json.encode(event.prevContent ?? ""), + event.eventId, + ], + ); + if (changes1 == 1 && changes2 == 1) return true; + } + return false; + } + + /// Stores an EventUpdate object in the database. Must be called inside of + /// [transaction]. + Future storeEventUpdate(EventUpdate eventUpdate) { + if (txn == null) return null; + Map eventContent = eventUpdate.content; + String type = eventUpdate.type; + String chat_id = eventUpdate.roomID; + + // Get the state_key for m.room.member events + String state_key = ""; + if (eventContent["state_key"] is String) { + state_key = eventContent["state_key"]; + } + + if (eventUpdate.eventType == "m.room.redaction") { + redactMessage(eventUpdate); + } + + if (type == "timeline" || type == "history") { + // calculate the status + num status = 2; + if (eventContent["status"] is num) status = eventContent["status"]; + + // Save the event in the database + if ((status == 1 || status == -1) && + eventContent["unsigned"] is Map && + eventContent["unsigned"]["transaction_id"] is String) + txn.rawUpdate( + "UPDATE Events SET status=?, event_id=? WHERE event_id=?", [ + status, + eventContent["event_id"], + eventContent["unsigned"]["transaction_id"] + ]); + else + txn.rawInsert( + "INSERT OR REPLACE INTO Events VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + [ + eventContent["event_id"], + chat_id, + eventContent["origin_server_ts"], + eventContent["sender"], + eventContent["type"], + json.encode(eventContent["unsigned"] ?? ""), + json.encode(eventContent["content"]), + json.encode(eventContent["prevContent"]), + eventContent["state_key"], + status + ]); + + // Is there a transaction id? Then delete the event with this id. + if (status != -1 && + eventUpdate.content.containsKey("unsigned") && + eventUpdate.content["unsigned"]["transaction_id"] is String) + txn.rawDelete("DELETE FROM Events WHERE event_id=?", + [eventUpdate.content["unsigned"]["transaction_id"]]); + } + + if (type == "history") return null; + + if (eventUpdate.content["event_id"] != null || + eventUpdate.content["state_key"] != null) { + final String now = DateTime.now().millisecondsSinceEpoch.toString(); + txn.rawInsert( + "INSERT OR REPLACE INTO RoomStates VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)", + [ + eventContent["event_id"] ?? now, + chat_id, + eventContent["origin_server_ts"] ?? now, + eventContent["sender"], + state_key, + json.encode(eventContent["unsigned"] ?? ""), + json.encode(eventContent["prev_content"] ?? ""), + eventContent["type"], + json.encode(eventContent["content"]), + ]); + } else + txn.rawInsert("INSERT OR REPLACE INTO RoomAccountData VALUES(?, ?, ?)", [ + eventContent["type"], + chat_id, + json.encode(eventContent["content"]), + ]); + + return null; + } + + /// Returns a User object by a given Matrix ID and a Room. + Future getUser({String matrixID, Room room}) async { + List> res = await _db.rawQuery( + "SELECT * FROM RoomStates WHERE state_key=? AND room_id=?", + [matrixID, room.id]); + if (res.length != 1) return null; + return RoomState.fromJson(res[0], room).asUser; + } + + /// Loads all Users in the database to provide a contact list + /// except users who are in the Room with the ID [exceptRoomID]. + Future> loadContacts({String exceptRoomID = ""}) async { + List> res = await _db.rawQuery( + "SELECT * FROM RoomStates WHERE state_key LIKE '@%:%' AND state_key!=? AND room_id!=? GROUP BY state_key ORDER BY state_key", + [client.userID, exceptRoomID]); + List userList = []; + for (int i = 0; i < res.length; i++) + userList + .add(RoomState.fromJson(res[i], Room(id: "", client: client)).asUser); + return userList; + } + + /// Returns all users of a room by a given [roomID]. + Future> loadParticipants(Room room) async { + List> res = await _db.rawQuery( + "SELECT * " + + " FROM RoomStates " + + " WHERE room_id=? " + + " AND type='m.room.member'", + [room.id]); + + List participants = []; + + for (num i = 0; i < res.length; i++) { + participants.add(RoomState.fromJson(res[i], room).asUser); + } + + return participants; + } + + /// Returns a list of events for the given room and sets all participants. + Future> getEventList(Room room) async { + List> eventRes = await _db.rawQuery( + "SELECT * " + + " FROM Events " + + " WHERE room_id=?" + + " GROUP BY event_id " + + " ORDER BY origin_server_ts DESC", + [room.id]); + + List eventList = []; + + for (num i = 0; i < eventRes.length; i++) + eventList.add(Event.fromJson(eventRes[i], room)); + + return eventList; + } + + /// Returns all rooms, the client is participating. Excludes left rooms. + Future> getRoomList({bool onlyLeft = false}) async { + List> res = await _db.rawQuery("SELECT * " + + " FROM Rooms" + + " WHERE membership" + + (onlyLeft ? "=" : "!=") + + "'leave' " + + " GROUP BY room_id "); + List roomList = []; + for (num i = 0; i < res.length; i++) { + Room room = await Room.getRoomFromTableRow( + res[i], + client, + states: getStatesFromRoomId(res[i]["room_id"]), + ); + roomList.add(room); + } + return roomList; + } + + /// Returns a room without events and participants. + @deprecated + Future getRoomById(String id) async { + List> res = + await _db.rawQuery("SELECT * FROM Rooms WHERE room_id=?", [id]); + if (res.length != 1) return null; + return Room.getRoomFromTableRow(res[0], client, + roomAccountData: getAccountDataFromRoomId(id), + states: getStatesFromRoomId(id)); + } + + Future>> getStatesFromRoomId(String id) async { + return _db.rawQuery("SELECT * FROM RoomStates WHERE room_id=?", [id]); + } + + Future>> getAccountDataFromRoomId(String id) async { + return _db.rawQuery("SELECT * FROM RoomAccountData WHERE room_id=?", [id]); + } + + Future resetNotificationCount(String roomID) async { + await _db.rawDelete( + "UPDATE Rooms SET notification_count=0, highlight_count=0 WHERE room_id=?", + [roomID]); + return; + } + + Future forgetRoom(String roomID) async { + await _db.rawDelete("DELETE FROM Rooms WHERE room_id=?", [roomID]); + return; + } + + /// Searches for the event in the store. + Future getEventById(String eventID, Room room) async { + List> res = await _db.rawQuery( + "SELECT * FROM Events WHERE event_id=? AND room_id=?", + [eventID, room.id]); + if (res.length == 0) return null; + return Event.fromJson(res[0], room); + } + + Future> getAccountData() async { + Map newAccountData = {}; + List> rawAccountData = + await _db.rawQuery("SELECT * FROM AccountData"); + for (int i = 0; i < rawAccountData.length; i++) + newAccountData[rawAccountData[i]["type"]] = + AccountData.fromJson(rawAccountData[i]); + return newAccountData; + } + + Future> getPresences() async { + Map newPresences = {}; + // TODO: Fix the json parsing of presences + /*List> rawPresences = + await _db.rawQuery("SELECT * FROM Presences"); + for (int i = 0; i < rawPresences.length; i++) + newPresences[rawPresences[i]["type"]] = + Presence.fromJson(rawPresences[i]);*/ + return newPresences; + } + + Future removeEvent(String eventId) async { + assert(eventId != ""); + await _db.rawDelete("DELETE FROM Events WHERE event_id=?", [eventId]); + return; + } + + Future forgetNotification(String roomID) async { + assert(roomID != ""); + await _db + .rawDelete("DELETE FROM NotificationsCache WHERE chat_id=?", [roomID]); + return; + } + + Future addNotification(String roomID, String event_id, int uniqueID) async { + assert(roomID != ""); + assert(event_id != ""); + assert(uniqueID != ""); + await _db.rawInsert( + "INSERT OR REPLACE INTO NotificationsCache(id, chat_id, event_id) VALUES (?, ?, ?)", + [uniqueID, roomID, event_id]); + // Make sure we got the same unique ID everywhere + await _db.rawUpdate("UPDATE NotificationsCache SET id=? WHERE chat_id=?", + [uniqueID, roomID]); + return; + } + + Future>> getNotificationByRoom( + String room_id) async { + assert(room_id != ""); + List> res = await _db.rawQuery( + "SELECT * FROM NotificationsCache WHERE chat_id=?", [room_id]); + if (res.length == 0) return null; + return res; + } + + static final Map schemes = { + /// The database scheme for the Client class. + "Clients": 'CREATE TABLE IF NOT EXISTS Clients(' + + 'client TEXT PRIMARY KEY, ' + + 'token TEXT, ' + + 'homeserver TEXT, ' + + 'matrix_id TEXT, ' + + 'device_id TEXT, ' + + 'device_name TEXT, ' + + 'prev_batch TEXT, ' + + 'matrix_versions TEXT, ' + + 'lazy_load_members INTEGER, ' + + 'UNIQUE(client))', + + /// The database scheme for the Room class. + 'Rooms': 'CREATE TABLE IF NOT EXISTS Rooms(' + + 'room_id TEXT PRIMARY KEY, ' + + 'membership TEXT, ' + + 'highlight_count INTEGER, ' + + 'notification_count INTEGER, ' + + 'prev_batch TEXT, ' + + 'joined_member_count INTEGER, ' + + 'invited_member_count INTEGER, ' + + 'heroes TEXT, ' + + 'UNIQUE(room_id))', + + /// The database scheme for the TimelineEvent class. + 'Events': 'CREATE TABLE IF NOT EXISTS Events(' + + 'event_id TEXT PRIMARY KEY, ' + + 'room_id TEXT, ' + + 'origin_server_ts INTEGER, ' + + 'sender TEXT, ' + + 'type TEXT, ' + + 'unsigned TEXT, ' + + 'content TEXT, ' + + 'prev_content TEXT, ' + + 'state_key TEXT, ' + + "status INTEGER, " + + 'UNIQUE(event_id))', + + /// The database scheme for room states. + 'RoomStates': 'CREATE TABLE IF NOT EXISTS RoomStates(' + + 'event_id TEXT PRIMARY KEY, ' + + 'room_id TEXT, ' + + 'origin_server_ts INTEGER, ' + + 'sender TEXT, ' + + 'state_key TEXT, ' + + 'unsigned TEXT, ' + + 'prev_content TEXT, ' + + 'type TEXT, ' + + 'content TEXT, ' + + 'UNIQUE(room_id,state_key,type))', + + /// The database scheme for room states. + 'AccountData': 'CREATE TABLE IF NOT EXISTS AccountData(' + + 'type TEXT PRIMARY KEY, ' + + 'content TEXT, ' + + 'UNIQUE(type))', + + /// The database scheme for room states. + 'RoomAccountData': 'CREATE TABLE IF NOT EXISTS RoomAccountData(' + + 'type TEXT PRIMARY KEY, ' + + 'room_id TEXT, ' + + 'content TEXT, ' + + 'UNIQUE(type,room_id))', + + /// The database scheme for room states. + 'Presences': 'CREATE TABLE IF NOT EXISTS Presences(' + + 'type TEXT PRIMARY KEY, ' + + 'sender TEXT, ' + + 'content TEXT, ' + + 'UNIQUE(sender))', + + /// The database scheme for the NotificationsCache class. + "NotificationsCache": 'CREATE TABLE IF NOT EXISTS NotificationsCache(' + + 'id int, ' + + 'chat_id TEXT, ' + // The chat id + 'event_id TEXT, ' + // The matrix id of the Event + 'UNIQUE(event_id))', + }; +} diff --git a/lib/views/chat.dart b/lib/views/chat.dart new file mode 100644 index 0000000..0261f48 --- /dev/null +++ b/lib/views/chat.dart @@ -0,0 +1,237 @@ +import 'dart:io'; + +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:fluffychat/components/adaptive_page_layout.dart'; +import 'package:fluffychat/components/chat_settings_popup_menu.dart'; +import 'package:fluffychat/components/list_items/message.dart'; +import 'package:fluffychat/components/matrix.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:toast/toast.dart'; + +import 'chat_list.dart'; + +class Chat extends StatefulWidget { + final String id; + + const Chat(this.id, {Key key}) : super(key: key); + @override + _ChatState createState() => _ChatState(); +} + +class _ChatState extends State { + Room room; + + Timeline timeline; + + final ScrollController _scrollController = new ScrollController(); + + @override + void initState() { + _scrollController.addListener(() async { + if (_scrollController.position.pixels == + _scrollController.position.maxScrollExtent) { + if (timeline.events.length > 0 && + timeline.events[timeline.events.length - 1].type != + EventTypes.RoomCreate) { + await timeline.requestHistory(historyCount: 100); + } + } + }); + + super.initState(); + } + + Future getTimeline() async { + timeline ??= await room.getTimeline(onUpdate: () { + setState(() {}); + }); + return true; + } + + @override + void dispose() { + timeline?.sub?.cancel(); + super.dispose(); + } + + final TextEditingController sendController = TextEditingController(); + + void send() { + if (sendController.text.isEmpty) return; + room.sendTextEvent(sendController.text); + sendController.text = ""; + } + + void sendFileAction(BuildContext context) async { + if (kIsWeb) { + return Toast.show("Not supported in web", context); + } + File file = await FilePicker.getFile(); + if (file == null) return; + Matrix.of(context).tryRequestWithLoadingDialog( + room.sendFileEvent( + MatrixFile(bytes: await file.readAsBytes(), path: file.path), + ), + ); + } + + void sendImageAction(BuildContext context) async { + if (kIsWeb) { + return Toast.show("Not supported in web", context); + } + File file = await ImagePicker.pickImage( + source: ImageSource.gallery, + imageQuality: 50, + maxWidth: 1600, + maxHeight: 1600); + if (file == null) return; + Matrix.of(context).tryRequestWithLoadingDialog( + room.sendImageEvent( + MatrixFile(bytes: await file.readAsBytes(), path: file.path), + ), + ); + } + + void openCameraAction(BuildContext context) async { + if (kIsWeb) { + return Toast.show("Not supported in web", context); + } + File file = await ImagePicker.pickImage( + source: ImageSource.camera, + imageQuality: 50, + maxWidth: 1600, + maxHeight: 1600); + if (file == null) return; + Matrix.of(context).tryRequestWithLoadingDialog( + room.sendImageEvent( + MatrixFile(bytes: await file.readAsBytes(), path: file.path), + ), + ); + } + + @override + Widget build(BuildContext context) { + Client client = Matrix.of(context).client; + room ??= client.getRoomById(widget.id); + + if (room.membership == Membership.invite) room.join(); + + return AdaptivePageLayout( + primaryPage: FocusPage.SECOND, + firstScaffold: ChatList( + activeChat: widget.id, + ), + secondScaffold: Scaffold( + appBar: AppBar( + title: Text(room.displayname), + actions: [ChatSettingsPopupMenu(room, !room.isDirectChat)], + ), + body: SafeArea( + child: Column( + children: [ + Expanded( + child: FutureBuilder( + future: getTimeline(), + builder: (BuildContext context, snapshot) { + if (!snapshot.hasData) + return Center( + child: CircularProgressIndicator(), + ); + if (room.notificationCount != null && + room.notificationCount > 0 && + timeline != null && + timeline.events.length > 0) + room.sendReadReceipt(timeline.events[0].eventId); + return ListView.builder( + reverse: true, + itemCount: timeline.events.length, + controller: _scrollController, + itemBuilder: (BuildContext context, int i) => + Message(timeline.events[i]), + ); + }, + ), + ), + Container( + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.2), + spreadRadius: 1, + blurRadius: 2, + offset: Offset(0, -1), // changes position of shadow + ), + ], + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + kIsWeb + ? Container() + : PopupMenuButton( + icon: Icon(Icons.add), + onSelected: (String choice) async { + if (choice == "file") + sendFileAction(context); + else if (choice == "image") + sendImageAction(context); + if (choice == "camera") openCameraAction(context); + }, + itemBuilder: (BuildContext context) => + >[ + const PopupMenuItem( + value: "file", + child: ListTile( + leading: Icon(Icons.attach_file), + title: Text('Send file'), + contentPadding: EdgeInsets.all(0), + ), + ), + const PopupMenuItem( + value: "image", + child: ListTile( + leading: Icon(Icons.image), + title: Text('Send image'), + contentPadding: EdgeInsets.all(0), + ), + ), + const PopupMenuItem( + value: "camera", + child: ListTile( + leading: Icon(Icons.camera), + title: Text('Open camera'), + contentPadding: EdgeInsets.all(0), + ), + ), + ], + ), + SizedBox(width: 8), + Expanded( + child: TextField( + onSubmitted: (t) => send(), + controller: sendController, + decoration: InputDecoration( + labelText: "Write a message...", + hintText: "You're message", + border: InputBorder.none, + ), + )), + SizedBox(width: 8), + IconButton( + icon: Icon(Icons.send), + onPressed: () => send(), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/views/chat_details.dart b/lib/views/chat_details.dart new file mode 100644 index 0000000..dc6587e --- /dev/null +++ b/lib/views/chat_details.dart @@ -0,0 +1,164 @@ +import 'dart:io'; + +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/components/adaptive_page_layout.dart'; +import 'package:fluffychat/components/chat_settings_popup_menu.dart'; +import 'package:fluffychat/components/content_banner.dart'; +import 'package:fluffychat/components/list_items/participant_list_item.dart'; +import 'package:fluffychat/components/matrix.dart'; +import 'package:fluffychat/utils/app_route.dart'; +import 'package:fluffychat/views/chat_list.dart'; +import 'package:fluffychat/views/invitation_selection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:toast/toast.dart'; + +class ChatDetails extends StatefulWidget { + final Room room; + + const ChatDetails(this.room); + + @override + _ChatDetailsState createState() => _ChatDetailsState(); +} + +class _ChatDetailsState extends State { + List members; + void setDisplaynameAction(BuildContext context, String displayname) async { + final MatrixState matrix = Matrix.of(context); + final Map success = + await matrix.tryRequestWithLoadingDialog( + widget.room.setName(displayname), + ); + if (success != null && success.length == 0) { + Toast.show( + "Displayname has been changed", + context, + duration: Toast.LENGTH_LONG, + ); + } + } + + void setAvatarAction(BuildContext context) async { + final File tempFile = await ImagePicker.pickImage( + source: ImageSource.gallery, + imageQuality: 50, + maxWidth: 1600, + maxHeight: 1600); + if (tempFile == null) return; + final MatrixState matrix = Matrix.of(context); + final Map success = + await matrix.tryRequestWithLoadingDialog( + widget.room.setAvatar( + MatrixFile( + bytes: await tempFile.readAsBytes(), + path: tempFile.path, + ), + ), + ); + if (success != null && success.length == 0) { + Toast.show( + "Avatar has been changed", + context, + duration: Toast.LENGTH_LONG, + ); + } + } + + void requestMoreMembersAction(BuildContext context) async { + final List participants = await Matrix.of(context) + .tryRequestWithLoadingDialog(widget.room.requestParticipants()); + if (participants != null) setState(() => members = participants); + } + + @override + Widget build(BuildContext context) { + members ??= widget.room.getParticipants(); + final int actualMembersCount = + widget.room.mInvitedMemberCount + widget.room.mJoinedMemberCount; + final bool canRequestMoreMembers = members.length < actualMembersCount; + widget.room.onUpdate = () => setState(() => members = null); + return AdaptivePageLayout( + primaryPage: FocusPage.SECOND, + firstScaffold: ChatList( + activeChat: widget.room.id, + ), + secondScaffold: Scaffold( + appBar: AppBar( + title: Text(widget.room.displayname), + actions: [ChatSettingsPopupMenu(widget.room, false)], + ), + body: ListView.builder( + itemCount: members.length + 1 + (canRequestMoreMembers ? 1 : 0), + itemBuilder: (BuildContext context, int i) => i == 0 + ? Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ContentBanner(widget.room.avatar), + widget.room.canSendEvent("m.room.avatar") && !kIsWeb + ? ListTile( + title: Text("Edit group avatar"), + trailing: Icon(Icons.file_upload), + onTap: () => setAvatarAction(context), + ) + : Container(), + widget.room.canSendEvent("m.room.name") + ? ListTile( + trailing: Icon(Icons.edit), + title: TextField( + textInputAction: TextInputAction.done, + onSubmitted: (s) => + setDisplaynameAction(context, s), + decoration: InputDecoration( + border: InputBorder.none, + labelText: "Edit group name", + labelStyle: TextStyle(color: Colors.black), + hintText: (widget.room.displayname), + ), + ), + ) + : Container(), + Row( + children: [ + Expanded( + child: Container(height: 8, color: Color(0xFFF8F8F8)), + ), + SizedBox(width: 8), + Text( + "$actualMembersCount participant(s)", + style: TextStyle( + fontSize: 15, + ), + ), + SizedBox(width: 8), + Expanded( + child: Container(height: 8, color: Color(0xFFF8F8F8)), + ), + ], + ), + ListTile( + title: Text("Invite contact"), + leading: Icon(Icons.add), + onTap: () => Navigator.of(context).push( + AppRoute.defaultRoute( + context, + InvitationSelection(widget.room), + ), + ), + ), + ], + ) + : i < members.length + 1 + ? ParticipantListItem(members[i - 1]) + : ListTile( + title: Text( + "Load more ${actualMembersCount - members.length} participants"), + leading: Icon(Icons.refresh), + onTap: () => requestMoreMembersAction(context), + ), + ), + ), + ); + } +} diff --git a/lib/views/chat_list.dart b/lib/views/chat_list.dart new file mode 100644 index 0000000..6650eea --- /dev/null +++ b/lib/views/chat_list.dart @@ -0,0 +1,135 @@ +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/components/adaptive_page_layout.dart'; +import 'package:fluffychat/components/dialogs/new_group_dialog.dart'; +import 'package:fluffychat/components/dialogs/new_private_chat_dialog.dart'; +import 'package:fluffychat/components/list_items/chat_list_item.dart'; +import 'package:fluffychat/components/matrix.dart'; +import 'package:fluffychat/utils/app_route.dart'; +import 'package:fluffychat/views/settings.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_speed_dial/flutter_speed_dial.dart'; + +class ChatListView extends StatelessWidget { + @override + Widget build(BuildContext context) { + return AdaptivePageLayout( + primaryPage: FocusPage.FIRST, + firstScaffold: ChatList(), + secondScaffold: Scaffold( + body: Center( + child: Icon(Icons.chat, size: 100, color: Color(0xFF5625BA)), + ), + ), + ); + } +} + +class ChatList extends StatefulWidget { + final String activeChat; + + const ChatList({this.activeChat, Key key}) : super(key: key); + @override + _ChatListState createState() => _ChatListState(); +} + +class _ChatListState extends State { + RoomList roomList; + + Future> getRooms(BuildContext context) async { + Client client = Matrix.of(context).client; + if (roomList != null) return roomList.rooms; + if (client.prevBatch?.isEmpty ?? true) + await client.connection.onFirstSync.stream.first; + roomList = client.getRoomList(onUpdate: () { + setState(() {}); + }); + return roomList.rooms; + } + + @override + void dispose() { + roomList?.eventSub?.cancel(); + roomList?.firstSyncSub?.cancel(); + roomList?.roomSub?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text( + "Conversations", + ), + actions: [ + IconButton( + icon: Icon(Icons.search), + onPressed: () {}, + ), + PopupMenuButton( + onSelected: (String choice) { + switch (choice) { + case "settings": + Navigator.of(context).push( + AppRoute.defaultRoute( + context, + SettingsView(), + ), + ); + } + }, + itemBuilder: (BuildContext context) => >[ + const PopupMenuItem( + value: "settings", + child: Text('Settings'), + ), + ], + ), + ], + ), + floatingActionButton: SpeedDial( + child: Icon(Icons.add), + backgroundColor: Color(0xFF5625BA), + children: [ + SpeedDialChild( + child: Icon(Icons.people_outline), + backgroundColor: Colors.blue, + label: 'Create new group', + labelStyle: TextStyle(fontSize: 18.0), + onTap: () => showDialog( + context: context, + builder: (BuildContext innerContext) => NewGroupDialog(), + ), + ), + SpeedDialChild( + child: Icon(Icons.chat_bubble_outline), + backgroundColor: Colors.green, + label: 'New private chat', + labelStyle: TextStyle(fontSize: 18.0), + onTap: () => showDialog( + context: context, + builder: (BuildContext innerContext) => NewPrivateChatDialog()), + ), + ], + ), + body: FutureBuilder>( + future: getRooms(context), + builder: (BuildContext context, snapshot) { + if (snapshot.hasData) { + List rooms = snapshot.data; + return ListView.builder( + itemCount: rooms.length, + itemBuilder: (BuildContext context, int i) => ChatListItem( + rooms[i], + activeChat: widget.activeChat == rooms[i].id, + ), + ); + } else + return Center( + child: CircularProgressIndicator(), + ); + }, + ), + ); + } +} diff --git a/lib/views/invitation_selection.dart b/lib/views/invitation_selection.dart new file mode 100644 index 0000000..545d5a2 --- /dev/null +++ b/lib/views/invitation_selection.dart @@ -0,0 +1,74 @@ +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/components/adaptive_page_layout.dart'; +import 'package:fluffychat/components/avatar.dart'; +import 'package:fluffychat/components/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:toast/toast.dart'; + +import 'chat_list.dart'; + +class InvitationSelection extends StatelessWidget { + final Room room; + const InvitationSelection(this.room, {Key key}) : super(key: key); + + Future> getContacts(BuildContext context) async { + final Client client = Matrix.of(context).client; + List participants = await room.requestParticipants(); + List contacts = []; + Map userMap = {}; + for (int i = 0; i < client.roomList.rooms.length; i++) { + List roomUsers = client.roomList.rooms[i].getParticipants(); + for (int j = 0; j < roomUsers.length; j++) { + if (userMap[roomUsers[j].id] != true && + participants.indexWhere((u) => u.id == roomUsers[j].id) == -1) + contacts.add(roomUsers[j]); + userMap[roomUsers[j].id] = true; + } + } + return contacts; + } + + void inviteAction(BuildContext context, String id) async { + final success = await Matrix.of(context).tryRequestWithLoadingDialog( + room.invite(id), + ); + if (success != false) + Toast.show( + "Contact has been invited to the group.", + context, + duration: Toast.LENGTH_LONG, + ); + } + + @override + Widget build(BuildContext context) { + final String groupName = room.name?.isEmpty ?? false ? "group" : room.name; + return AdaptivePageLayout( + primaryPage: FocusPage.SECOND, + firstScaffold: ChatList(activeChat: room.id), + secondScaffold: Scaffold( + appBar: AppBar( + title: Text("Invite contact to $groupName"), + ), + body: FutureBuilder>( + future: getContacts(context), + builder: (BuildContext context, snapshot) { + if (!snapshot.hasData) + return Center( + child: CircularProgressIndicator(), + ); + List contacts = snapshot.data; + return ListView.builder( + itemCount: contacts.length, + itemBuilder: (BuildContext context, int i) => ListTile( + leading: Avatar(contacts[i].avatarUrl), + title: Text(contacts[i].calcDisplayname()), + subtitle: Text(contacts[i].id), + onTap: () => inviteAction(context, contacts[i].id), + ), + ); + }, + )), + ); + } +} diff --git a/lib/views/login.dart b/lib/views/login.dart new file mode 100644 index 0000000..bcfc151 --- /dev/null +++ b/lib/views/login.dart @@ -0,0 +1,144 @@ +import 'dart:math'; + +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/components/matrix.dart'; +import 'package:flutter/material.dart'; + +const String defaultHomeserver = "https://matrix.org"; + +class LoginPage extends StatefulWidget { + @override + _LoginPageState createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + final TextEditingController usernameController = TextEditingController(); + final TextEditingController passwordController = TextEditingController(); + final TextEditingController serverController = + TextEditingController(text: "matrix.org"); + String usernameError; + String passwordError; + String serverError; + + void login(BuildContext context) async { + MatrixState matrix = Matrix.of(context); + if (usernameController.text.isEmpty) { + setState(() => usernameError = "Please enter your username."); + print("Please enter your username."); + } else { + setState(() => usernameError = null); + } + if (passwordController.text.isEmpty) { + setState(() => passwordError = "Please enter your password."); + } else { + setState(() => passwordError = null); + } + serverError = null; + + if (usernameController.text.isEmpty || passwordController.text.isEmpty) + return; + + String homeserver = serverController.text; + if (homeserver.isEmpty) homeserver = defaultHomeserver; + if (!homeserver.startsWith("https://")) + homeserver = "https://" + homeserver; + + try { + matrix.showLoadingDialog(context); + if (!await matrix.client.checkServer(homeserver)) { + setState(() => serverError = "Homeserver is not compatible."); + + return matrix.hideLoadingDialog(); + } + } catch (exception) { + setState(() => serverError = "Connection attempt failed!"); + return matrix.hideLoadingDialog(); + } + try { + await matrix.client + .login(usernameController.text, passwordController.text); + } on MatrixException catch (exception) { + setState(() => passwordError = exception.errorMessage); + return matrix.hideLoadingDialog(); + } catch (exception) { + setState(() => passwordError = exception.toString()); + return matrix.hideLoadingDialog(); + } + Matrix.of(context).saveAccount(); + matrix.hideLoadingDialog(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: TextField( + controller: serverController, + decoration: InputDecoration( + icon: Icon(Icons.domain), + hintText: "matrix.org", + errorText: serverError, + errorMaxLines: 1, + prefixText: "https://", + labelText: serverError == null ? "Homeserver" : serverError), + ), + ), + body: ListView( + padding: EdgeInsets.symmetric( + vertical: 16, + horizontal: max((MediaQuery.of(context).size.width - 600) / 2, 16)), + children: [ + Image.asset("assets/fluffychat-banner.png"), + TextField( + controller: usernameController, + decoration: InputDecoration( + hintText: "@username:domain", + icon: Icon(Icons.account_box), + errorText: usernameError, + labelText: "Username"), + ), + TextField( + controller: passwordController, + obscureText: true, + onSubmitted: (t) => login(context), + decoration: InputDecoration( + icon: Icon(Icons.vpn_key), + hintText: "****", + errorText: passwordError, + labelText: "Password"), + ), + SizedBox(height: 20), + Card( + elevation: 7, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(50), + ), + child: Container( + width: 120.0, + height: 50.0, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(50), + gradient: LinearGradient( + begin: Alignment.bottomLeft, + end: Alignment.topRight, + colors: [ + Colors.blue, + Color(0xFF5625BA), + ], + ), + ), + child: RawMaterialButton( + onPressed: () => login(context), + splashColor: Colors.grey, + child: Text( + "Login", + style: TextStyle(color: Colors.white, fontSize: 20.0), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/views/settings.dart b/lib/views/settings.dart new file mode 100644 index 0000000..344ceab --- /dev/null +++ b/lib/views/settings.dart @@ -0,0 +1,141 @@ +import 'dart:io'; + +import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/components/adaptive_page_layout.dart'; +import 'package:fluffychat/components/content_banner.dart'; +import 'package:fluffychat/components/matrix.dart'; +import 'package:fluffychat/utils/app_route.dart'; +import 'package:fluffychat/views/chat_list.dart'; +import 'package:fluffychat/views/login.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:toast/toast.dart'; + +class SettingsView extends StatelessWidget { + @override + Widget build(BuildContext context) { + return AdaptivePageLayout( + primaryPage: FocusPage.SECOND, + firstScaffold: ChatList(), + secondScaffold: Settings(), + ); + } +} + +class Settings extends StatefulWidget { + @override + _SettingsState createState() => _SettingsState(); +} + +class _SettingsState extends State { + Future profileFuture; + dynamic profile; + void logoutAction(BuildContext context) async { + MatrixState matrix = Matrix.of(context); + Navigator.of(context).pushAndRemoveUntil( + AppRoute.defaultRoute(context, LoginPage()), (r) => false); + await matrix.tryRequestWithLoadingDialog(matrix.client.logout()); + matrix.clean(); + } + + void setDisplaynameAction(BuildContext context, String displayname) async { + final MatrixState matrix = Matrix.of(context); + final Map success = + await matrix.tryRequestWithLoadingDialog( + matrix.client.connection.jsonRequest( + type: HTTPType.PUT, + action: "/client/r0/profile/${matrix.client.userID}/displayname", + data: {"displayname": displayname}, + ), + ); + if (success != null && success.length == 0) { + Toast.show( + "Displayname has been changed", + context, + duration: Toast.LENGTH_LONG, + ); + setState(() { + profileFuture = null; + profile = null; + }); + } + } + + void setAvatarAction(BuildContext context) async { + final File tempFile = await ImagePicker.pickImage( + source: ImageSource.gallery, + imageQuality: 50, + maxWidth: 1600, + maxHeight: 1600); + if (tempFile == null) return; + final MatrixState matrix = Matrix.of(context); + final Map success = + await matrix.tryRequestWithLoadingDialog( + matrix.client.setAvatar( + MatrixFile( + bytes: await tempFile.readAsBytes(), + path: tempFile.path, + ), + ), + ); + if (success != null && success.length == 0) { + Toast.show( + "Avatar has been changed", + context, + duration: Toast.LENGTH_LONG, + ); + setState(() { + profileFuture = null; + profile = null; + }); + } + } + + @override + Widget build(BuildContext context) { + final Client client = Matrix.of(context).client; + profileFuture ??= client.getProfileFromUserId(client.userID); + profileFuture.then((p) => setState(() => profile = p)); + return Scaffold( + appBar: AppBar( + title: Text("Settings"), + ), + body: ListView( + children: [ + ContentBanner( + profile?.avatarUrl ?? MxContent(""), + defaultIcon: Icons.account_circle, + loading: profile == null, + ), + kIsWeb + ? Container() + : ListTile( + title: Text("Upload avatar"), + trailing: Icon(Icons.file_upload), + onTap: () => setAvatarAction(context), + ), + ListTile( + trailing: Icon(Icons.edit), + title: TextField( + readOnly: profile == null, + textInputAction: TextInputAction.done, + onSubmitted: (s) => setDisplaynameAction(context, s), + decoration: InputDecoration( + border: InputBorder.none, + labelText: "Edit displayname", + labelStyle: TextStyle(color: Colors.black), + hintText: (profile?.displayname ?? ""), + ), + ), + ), + ListTile( + trailing: Icon(Icons.exit_to_app), + title: Text("Logout"), + onTap: () => logoutAction(context), + ), + ], + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..41fdbe0 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,385 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.11" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.2" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.4.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + bubble: + dependency: "direct main" + description: + name: bubble + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.9+1" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.14.11" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" + famedlysdk: + dependency: "direct main" + description: + path: "." + ref: "4cbfec1ac4c13e6cc72ee98e8290fd802a96cf89" + resolved-ref: "4cbfec1ac4c13e6cc72ee98e8290fd802a96cf89" + url: "https://gitlab.com/famedly/famedlysdk.git" + source: git + version: "0.0.1" + file_picker: + dependency: "direct main" + description: + name: file_picker + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.3+2" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.3" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.4" + flutter_responsive_screen: + dependency: "direct main" + description: + name: flutter_responsive_screen + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + flutter_speed_dial: + dependency: "direct main" + description: + name: flutter_speed_dial + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.5" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.0+3" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.3" + image: + dependency: transitive + description: + name: image + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.4" + image_picker: + dependency: "direct main" + description: + name: image_picker + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.2+3" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "2.4.0" + localstorage: + dependency: "direct main" + description: + name: localstorage + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1+4" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.6" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.8" + mime_type: + dependency: transitive + description: + name: mime_type + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.4" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.4" + path_provider: + dependency: transitive + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.1" + pedantic: + dependency: transitive + description: + name: pedantic + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0+1" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "2.4.0" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.1" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.5" + sqflite: + dependency: "direct main" + description: + name: sqflite + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.3" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + synchronized: + dependency: transitive + description: + name: synchronized + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.11" + toast: + dependency: "direct main" + description: + name: toast + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.5" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.6" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + url: "https://pub.dartlang.org" + source: hosted + version: "5.4.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1+2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0+2" + uuid: + dependency: transitive + description: + name: uuid + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.8" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "3.5.0" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" +sdks: + dart: ">=2.6.0 <3.0.0" + flutter: ">=1.12.8 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..d9947fb --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,96 @@ +name: fluffychat +description: Chat with your friends. + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +version: 0.1.0+1 + +environment: + sdk: ">=2.1.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + + flutter_responsive_screen: ^1.0.0 + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^0.1.2 + + famedlysdk: + git: + url: https://gitlab.com/famedly/famedlysdk.git + ref: 4cbfec1ac4c13e6cc72ee98e8290fd802a96cf89 + + localstorage: ^3.0.1+4 + bubble: ^1.1.9+1 + toast: ^0.1.5 + file_picker: ^1.4.3+2 + image_picker: ^0.6.2+3 + flutter_speed_dial: ^1.2.5 + url_launcher: ^5.4.1 + sqflite: ^1.2.0 + cached_network_image: ^2.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + + flutter_launcher_icons: "^0.7.3" + + +flutter_icons: + android: "launcher_icon" + ios: true + image_path: "assets/logo.png" + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + assets: + - assets/fluffychat-banner.png + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..ba00aa5 --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:fluffychat/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..79a6245 --- /dev/null +++ b/web/index.html @@ -0,0 +1,10 @@ + + + + + fluffychat + + + + +