├── .github
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── .gitignore
├── .gradle
└── 4.10.2
│ ├── fileChanges
│ └── last-build.bin
│ ├── fileHashes
│ └── fileHashes.lock
│ └── gc.properties
├── .metadata
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── analysis_options.yaml
├── android
├── README.md
├── app
│ ├── build.gradle
│ └── src
│ │ ├── debug
│ │ ├── AndroidManifest.xml
│ │ └── res
│ │ │ └── values
│ │ │ └── strings.xml
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── kotlin
│ │ │ └── com
│ │ │ │ └── example
│ │ │ │ └── circles_app
│ │ │ │ ├── ImageResize.kt
│ │ │ │ ├── MainActivity.kt
│ │ │ │ ├── PermissionHandler.kt
│ │ │ │ ├── ThumbnailUtils.kt
│ │ │ │ └── UploadPlatform.kt
│ │ └── res
│ │ │ ├── drawable-hdpi
│ │ │ └── ic_launcher_foreground.png
│ │ │ ├── drawable-mdpi
│ │ │ └── ic_launcher_foreground.png
│ │ │ ├── drawable-nodpi
│ │ │ └── splash.png
│ │ │ ├── drawable-xhdpi
│ │ │ └── ic_launcher_foreground.png
│ │ │ ├── drawable-xxhdpi
│ │ │ └── ic_launcher_foreground.png
│ │ │ ├── drawable-xxxhdpi
│ │ │ └── ic_launcher_foreground.png
│ │ │ ├── drawable
│ │ │ └── launch_background.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ └── ic_launcher.xml
│ │ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── launcher_icon.png
│ │ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── launcher_icon.png
│ │ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── launcher_icon.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── launcher_icon.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── launcher_icon.png
│ │ │ ├── values-de
│ │ │ └── strings.xml
│ │ │ └── values
│ │ │ ├── colors.xml
│ │ │ ├── strings.xml
│ │ │ └── styles.xml
│ │ └── profile
│ │ └── AndroidManifest.xml
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ └── gradle-wrapper.properties
├── settings.gradle
└── store
│ ├── feature.png
│ ├── hi-res-icon.png
│ ├── screenshot1.png
│ └── screenshot2.png
├── assets
├── graphics
│ ├── avatar_no_picture.png
│ ├── calendar
│ │ └── calendar_today.png
│ ├── channel
│ │ ├── create_new_channel.png
│ │ ├── details_date.png
│ │ ├── details_location.png
│ │ ├── details_members.png
│ │ ├── details_padlock.png
│ │ ├── event_joined.png
│ │ ├── event_open.png
│ │ ├── header_calendar_icon.png
│ │ ├── padlock.png
│ │ ├── rsvp
│ │ │ ├── rsvp_maybe.png
│ │ │ ├── rsvp_maybe_large.png
│ │ │ ├── rsvp_no_large.png
│ │ │ ├── rsvp_yes.png
│ │ │ └── rsvp_yes_large.png
│ │ ├── topic_joined.png
│ │ └── topic_open.png
│ ├── drawer
│ │ ├── account.png
│ │ ├── create_topic.png
│ │ ├── direct_message.png
│ │ ├── events.png
│ │ └── settings.png
│ ├── icon_notification.png
│ ├── icon_smile.png
│ ├── input
│ │ ├── checkbox_active.png
│ │ ├── checkbox_inactive.png
│ │ ├── icon_add_content.png
│ │ ├── icon_camera.png
│ │ └── icon_pictures.png
│ ├── menu_icon.png
│ ├── menu_more_icon.png
│ ├── update_indicator_darkgreen.png
│ ├── updates_indicator.png
│ ├── updates_indicator_white.png
│ ├── upload
│ │ ├── indicator_0_try_again.png
│ │ └── selected.png
│ ├── visual_twist.png
│ └── visual_twist_white_petrol.png
├── icon
│ └── icon.png
└── placeholder
│ ├── 2.0x
│ └── user_image_placeholder.png
│ ├── 3.0x
│ └── user_image_placeholder.png
│ └── user_image_placeholder.png
├── firebase
├── .gitignore
├── README.md
├── firebase.json
├── firestore.indexes.json
├── firestore.rules
├── functions
│ ├── .gitignore
│ ├── admin.js
│ ├── calendar-update.js
│ ├── channel-edit.js
│ ├── channel-flagChannelUnread.js
│ ├── channel-new-user.js
│ ├── channel-new.js
│ ├── channel-remove-user.js
│ ├── channel-remove.js
│ ├── channel-updatedAt.js
│ ├── channel-util.js
│ ├── constants.js
│ ├── cron
│ │ └── event-notify-about-upcoming-event.js
│ ├── group-update.js
│ ├── group-util.js
│ ├── index.js
│ ├── localize-util.js
│ ├── message-get.js
│ ├── message-new.js
│ ├── message-update.js
│ ├── package-lock.json
│ ├── package.json
│ ├── push-send.js
│ ├── reaction-push.js
│ ├── rsvp-update.js
│ ├── user-groupUpdate.js
│ ├── user-new.js
│ └── user-util.js
├── package-lock.json
└── scripts
│ └── group-create.js
├── firestore-1.png
├── firestore-2.png
├── firestore-3.png
├── firestore-4.png
├── fonts
├── Edmondsans-Bold.otf
├── Edmondsans-Medium.otf
├── Edmondsans-Regular.otf
├── Poppins-ExtraBold.ttf
└── Poppins-Regular.ttf
├── ios
├── Flutter
│ ├── AppFrameworkInfo.plist
│ ├── Debug.xcconfig
│ └── Release.xcconfig
├── Podfile
├── Podfile.lock
├── Runner.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ └── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Runner.xcscheme
├── Runner.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── WorkspaceSettings.xcsettings
└── Runner
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── Icon-60.png
│ │ ├── Icon-60@2x.png
│ │ ├── Icon-60@3x.png
│ │ ├── Icon-76.png
│ │ ├── Icon-76@2x.png
│ │ ├── Icon-Small-1.png
│ │ ├── Icon-Small.png
│ │ ├── Icon-Small20.png
│ │ ├── Icon-Small@2x-1.png
│ │ ├── Icon-Small@2x.png
│ │ ├── Icon-Small@3x.png
│ │ ├── Icon-Spotlight-40.png
│ │ ├── Icon-Spotlight-40@2x-1.png
│ │ ├── Icon-Spotlight-40@2x.png
│ │ ├── Icon-Spotlight-40@3x.png
│ │ ├── Icon-Spotlight-41.png
│ │ ├── Icon-Spotlight-42.png
│ │ ├── Icon-iPadPro@2x.png
│ │ └── timy-icon.png
│ ├── Contents.json
│ └── LaunchImage.imageset
│ │ ├── Contents.json
│ │ └── splash screen.pdf
│ ├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
│ ├── ImageProcessingService.swift
│ ├── Info-dev.plist
│ ├── Info.plist
│ ├── PermissionService.swift
│ ├── Runner-Bridging-Header.h
│ ├── Runner-dev.entitlements
│ ├── Runner.entitlements
│ ├── UploadService.swift
│ ├── de.lproj
│ ├── InfoPlist.strings
│ ├── LaunchScreen.strings
│ └── Main.strings
│ └── en.lproj
│ └── InfoPlist.strings
├── lib
├── circles_app.dart
├── circles_localization.dart
├── cupertinoLocalizationDelegate.dart
├── data
│ ├── calendar_repository.dart
│ ├── channel_repository.dart
│ ├── file_repository.dart
│ ├── firestore_paths.dart
│ ├── group_repository.dart
│ ├── message_repository.dart
│ └── user_repository.dart
├── domain
│ └── redux
│ │ ├── app_actions.dart
│ │ ├── app_middleware.dart
│ │ ├── app_reducer.dart
│ │ ├── app_selector.dart
│ │ ├── app_state.dart
│ │ ├── app_state.g.dart
│ │ ├── attachment
│ │ ├── attachment_actions.dart
│ │ ├── attachment_middleware.dart
│ │ └── image_processor.dart
│ │ ├── authentication
│ │ ├── auth_actions.dart
│ │ ├── auth_middleware.dart
│ │ └── auth_reducer.dart
│ │ ├── calendar
│ │ ├── calendar_actions.dart
│ │ ├── calendar_middleware.dart
│ │ └── calendar_reducer.dart
│ │ ├── channel
│ │ ├── channel_actions.dart
│ │ ├── channel_middleware.dart
│ │ └── channel_reducer.dart
│ │ ├── message
│ │ ├── message_actions.dart
│ │ ├── message_middleware.dart
│ │ └── message_reducer.dart
│ │ ├── push
│ │ ├── push_actions.dart
│ │ ├── push_middleware.dart
│ │ └── push_reducer.dart
│ │ ├── stream_subscriptions.dart
│ │ ├── ui
│ │ ├── ui_actions.dart
│ │ ├── ui_reducer.dart
│ │ ├── ui_state.dart
│ │ ├── ui_state.g.dart
│ │ └── ui_state_selector.dart
│ │ └── user
│ │ ├── user_actions.dart
│ │ ├── user_middleware.dart
│ │ └── user_reducer.dart
├── main.dart
├── model
│ ├── calendar_entry.dart
│ ├── calendar_entry.g.dart
│ ├── channel.dart
│ ├── channel.g.dart
│ ├── channel_state.dart
│ ├── channel_state.g.dart
│ ├── group.dart
│ ├── group.g.dart
│ ├── in_app_notification.dart
│ ├── in_app_notification.g.dart
│ ├── message.dart
│ ├── message.g.dart
│ ├── reaction.dart
│ ├── reaction.g.dart
│ ├── user.dart
│ └── user.g.dart
├── native_channels
│ ├── android_permission_channel.dart
│ ├── android_thumbnail_channel.dart
│ ├── ios_permission_channel.dart
│ └── upload_platform.dart
├── presentation
│ ├── calendar
│ │ ├── calendar_item.dart
│ │ ├── calendar_screen.dart
│ │ ├── calendar_screen_viewmodel.dart
│ │ └── calendar_screen_viewmodel.g.dart
│ ├── channel
│ │ ├── channel_screen.dart
│ │ ├── channel_screen_viewmodel.dart
│ │ ├── channel_screen_viewmodel.g.dart
│ │ ├── create
│ │ │ └── create_channel.dart
│ │ ├── details
│ │ │ ├── topic_details.dart
│ │ │ ├── topic_details_viewmodel.dart
│ │ │ └── topic_details_viewmodel.g.dart
│ │ ├── event
│ │ │ ├── create_event.dart
│ │ │ ├── event_details.dart
│ │ │ ├── event_details_viewmodel.dart
│ │ │ ├── event_details_viewmodel.g.dart
│ │ │ ├── rsvp_dialog.dart
│ │ │ └── rsvp_header.dart
│ │ ├── input
│ │ │ ├── attach_button.dart
│ │ │ ├── chat_input.dart
│ │ │ ├── chat_input_viewmodel.dart
│ │ │ ├── chat_input_viewmodel.g.dart
│ │ │ └── send_button.dart
│ │ ├── invite
│ │ │ ├── invite_to_channel_screen.dart
│ │ │ ├── invite_to_channel_viewmodel.dart
│ │ │ └── invite_to_channel_viewmodel.g.dart
│ │ ├── join_channel.dart
│ │ ├── message
│ │ │ ├── media
│ │ │ │ └── MediaMessage.dart
│ │ │ ├── message_body.dart
│ │ │ ├── message_item.dart
│ │ │ ├── message_timestamp.dart
│ │ │ └── system_message_item.dart
│ │ ├── messages_list
│ │ │ ├── messages_list.dart
│ │ │ ├── messages_list_viewmodel.dart
│ │ │ └── messages_list_viewmodel.g.dart
│ │ ├── messages_scroll_controller.dart
│ │ ├── messages_section.dart
│ │ └── reaction
│ │ │ ├── emoji_picker.dart
│ │ │ ├── reaction.dart
│ │ │ ├── reaction_button.dart
│ │ │ ├── reaction_detail_data.dart
│ │ │ ├── reaction_detail_data.g.dart
│ │ │ ├── reaction_details.dart
│ │ │ └── reaction_section.dart
│ ├── common
│ │ ├── color_label_text_form_field.dart
│ │ ├── common_app_bar.dart
│ │ ├── date_form_field.dart
│ │ ├── error_label_text_form_field.dart
│ │ ├── modal_item.dart
│ │ ├── platform_alerts.dart
│ │ ├── round_button.dart
│ │ └── time_form_field.dart
│ ├── home
│ │ ├── channel_list
│ │ │ ├── channel_list.dart
│ │ │ ├── channel_list_item.dart
│ │ │ ├── channel_list_viewmodel.dart
│ │ │ ├── channel_list_viewmodel.g.dart
│ │ │ ├── event_status_icon_widget.dart
│ │ │ └── group_status_icon_widget.dart
│ │ ├── circles_drawer.dart
│ │ ├── group_list
│ │ │ ├── group_list.dart
│ │ │ ├── group_list_viewmodel.dart
│ │ │ └── group_list_viewmodel.g.dart
│ │ ├── home_app_bar.dart
│ │ ├── home_app_bar_viewmodel.dart
│ │ ├── home_app_bar_viewmodel.g.dart
│ │ ├── homescreen.dart
│ │ ├── in_app_notification
│ │ │ ├── in_app_notification_viewmodel.dart
│ │ │ ├── in_app_notification_viewmodel.g.dart
│ │ │ └── in_app_notification_widget.dart
│ │ ├── main_screen.dart
│ │ ├── main_screen_viewmodel.dart
│ │ ├── main_screen_viewmodel.g.dart
│ │ └── slide_out_screen.dart
│ ├── image
│ │ ├── file_picker_item.dart
│ │ ├── file_picker_screen.dart
│ │ ├── image_pinch_screen.dart
│ │ ├── image_screen.dart
│ │ └── image_with_loader.dart
│ ├── login
│ │ ├── auth_button.dart
│ │ ├── auth_button_container.dart
│ │ └── loginscreen.dart
│ ├── settings
│ │ ├── privacy_settings_button.dart
│ │ └── settings_screen.dart
│ └── user
│ │ ├── profile_avatar.dart
│ │ ├── rsvp_icon.dart
│ │ ├── selected_item.dart
│ │ ├── user_avatar.dart
│ │ ├── user_item.dart
│ │ ├── user_screen.dart
│ │ ├── user_screen_viewmodel.dart
│ │ └── user_screen_viewmodel.g.dart
├── routes.dart
├── theme.dart
└── util
│ ├── HexColor.dart
│ ├── cache.dart
│ ├── date_formatting.dart
│ ├── logger.dart
│ └── permissions.dart
├── pubspec.lock
├── pubspec.yaml
├── res
└── values
│ └── strings_en.arb
├── test
├── data
│ ├── FirestoreMocks.dart
│ ├── channel_repository_test.dart
│ ├── circle_repository_test.dart
│ ├── data_mocks.dart
│ ├── message_repository_test.dart
│ └── user_repository_test.dart
├── domain
│ ├── auth_middleware_test.dart
│ ├── auth_reducer_test.dart
│ ├── channel
│ │ └── channel_middleware_test.dart
│ ├── message
│ │ ├── message_middleware_test.dart
│ │ └── message_reducer_test.dart
│ ├── middleware_test.dart
│ ├── push
│ │ ├── push_middleware_test.dart
│ │ └── push_reducer_test.dart
│ ├── reducer_test.dart
│ ├── redux_mocks.dart
│ └── user
│ │ ├── user_middleware_test.dart
│ │ └── user_reducer_test.dart
├── model
│ └── message_test.dart
├── presentation
│ ├── channel
│ │ ├── create
│ │ │ └── create_channel_test.dart
│ │ ├── event
│ │ │ └── create_event_test.dart
│ │ └── invite
│ │ │ └── invite_to_channel_viewmodel_test.dart
│ ├── home
│ │ └── channel_list_viewmodel_test.dart
│ └── reaction
│ │ └── reaction_details_test.dart
├── synchronous_error.dart
└── widget_test.dart
└── timy.png
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Before reporting a bug, be sure that you followed the README.md instructions.**
11 |
12 | If you still had a bug after following the instructions, please go ahead filling this template.
13 |
14 | All incomplete bug reports will be marked as 'invalid' and closed.
15 |
16 | **Describe the bug**
17 |
18 | A clear and concise description of what the bug is.
19 |
20 | **To Reproduce**
21 |
22 | Steps to reproduce the behavior:
23 | 1. Go to '...'
24 | 2. Click on '....'
25 | 3. Scroll down to '....'
26 | 4. See error
27 |
28 | **Expected behavior**
29 |
30 | A clear and concise description of what you expected to happen.
31 |
32 | **Screenshots**
33 |
34 | If applicable, add screenshots to help explain your problem.
35 |
36 | **Device Information:**
37 |
38 | - Device: [e.g. iPhone6]
39 | - OS: [e.g. iOS8.1]
40 |
41 | **Flutter Information**
42 |
43 | - `flutter doctor` output
44 |
45 | **Logs**
46 |
47 | ```
48 | Paste relevant output logs inside this quotes
49 | ```
50 |
51 | **Additional context**
52 |
53 | Add any other context about the problem here.
54 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | This project is no longer under development, but we are still open to ideas and maybe someone wants to build them.
11 | That's why we would like to hear from you if you would like to have a new feature.
12 |
13 | **Is your feature request related to a problem? Please describe.**
14 |
15 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
16 |
17 | **Describe the solution you'd like**
18 |
19 | A clear and concise description of what you want to happen.
20 |
21 | **Describe alternatives you've considered**
22 |
23 | A clear and concise description of any alternative solutions or features you've considered.
24 |
25 | **Additional context**
26 |
27 | Add any other context or screenshots about the feature request here.
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 |
12 | # IntelliJ related
13 | *.iml
14 | *.ipr
15 | *.iws
16 | .idea/
17 |
18 | # Visual Studio Code related
19 | .vscode/
20 |
21 | # Flutter/Dart/Pub related
22 | **/doc/api/
23 | .dart_tool/
24 | .flutter-plugins
25 | .packages
26 | .pub-cache/
27 | .pub/
28 | /build/
29 |
30 | # Android related
31 | **/android/**/gradle-wrapper.jar
32 | **/android/.gradle
33 | **/android/captures/
34 | **/android/gradlew
35 | **/android/gradlew.bat
36 | **/android/local.properties
37 | **/android/**/GeneratedPluginRegistrant.java
38 | **/android/**/google-services.json
39 |
40 | # iOS/XCode related
41 | **/ios/**/*.mode1v3
42 | **/ios/**/*.mode2v3
43 | **/ios/**/*.moved-aside
44 | **/ios/**/*.pbxuser
45 | **/ios/**/*.perspectivev3
46 | **/ios/**/*sync/
47 | **/ios/**/.sconsign.dblite
48 | **/ios/**/.tags*
49 | **/ios/**/.vagrant/
50 | **/ios/**/DerivedData/
51 | **/ios/**/Icon?
52 | **/ios/**/Pods/
53 | **/ios/**/.symlinks/
54 | **/ios/**/profile
55 | **/ios/**/xcuserdata
56 | **/ios/.generated/
57 | **/ios/Flutter/App.framework
58 | **/ios/Flutter/Flutter.framework
59 | **/ios/Flutter/Generated.xcconfig
60 | **/ios/Flutter/app.flx
61 | **/ios/Flutter/app.zip
62 | **/ios/Flutter/flutter_assets/
63 | **/ios/ServiceDefinitions.json
64 | **/ios/Runner/GeneratedPluginRegistrant.*
65 | **/ios/Flutter/flutter_export_environment.sh
66 |
67 | # Exceptions to above rules.
68 | !**/ios/**/default.mode1v3
69 | !**/ios/**/default.mode2v3
70 | !**/ios/**/default.pbxuser
71 | !**/ios/**/default.perspectivev3
72 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
73 | janoodle-prod-firebase-adminsdk.json
74 |
--------------------------------------------------------------------------------
/.gradle/4.10.2/fileChanges/last-build.bin:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gradle/4.10.2/fileHashes/fileHashes.lock:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/.gradle/4.10.2/fileHashes/fileHashes.lock
--------------------------------------------------------------------------------
/.gradle/4.10.2/gc.properties:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/.gradle/4.10.2/gc.properties
--------------------------------------------------------------------------------
/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: 7a4c33425ddd78c54aba07d86f3f9a4a0051769b
8 | channel: stable
9 |
10 | project_type: app
11 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Timy Messenger
2 |
3 | :+1::tada: First off, thanks for taking the time to contribute! :tada::+1:
4 |
5 | The following is a guideline for contributing to Timy Messenger.
6 | These are mostly guidelines, not rules. Use your best judgment,
7 | and feel free to propose changes to this document in a pull request.
8 |
9 | ## Expectation vs Reality
10 |
11 | Timy Messenger is an open source app created originally
12 | by Janoodle Unlimited GmbH as part of an Minimum Viable Product test.
13 | It is currently Open Source and available for you to use, change and play with.
14 |
15 | However, **this project is no longer under development**,
16 | which means that there's no plan to add new features to it.
17 | The original developers are still maintainers of the project and will keep an eye on it, but don't expect quick responses.
18 |
19 | ## How Can I Contribute?
20 |
21 | ### Reporting Bugs
22 |
23 | If you notice something not working, feel free to report a bug using the "Report Bug" template.
24 |
25 | **All bugs reported that don't follow the template will be marked as 'invalid' and closed.**
26 |
27 | ### Adding Documentation
28 |
29 | Any sort of documentation is welcome.
30 |
31 | For example:
32 |
33 | - Setup instructions.
34 | - Documenting methods with useful comments.
35 | - Links to external resources like blog posts or videos talking about the app.
36 |
37 | ### Providing Translations
38 |
39 | We are happy to add more translations to the app.
40 |
41 | ### Fixing Reported Bugs
42 |
43 | We are happy to add any fixes to the reported bugs in "Issues".
44 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # Specify analysis options.
2 | #
3 | # For a list of lints, see: http://dart-lang.github.io/linter/lints/
4 | # See the configuration guide for more
5 | # https://github.com/dart-lang/sdk/tree/master/pkg/analyzer#configuring-the-analyzer
6 | #
7 | # There are four similar analysis options files in the flutter repos:
8 | # - analysis_options.yaml
9 | # - packages/flutter/lib/analysis_options_user.yaml (this file)
10 | # - https://github.com/flutter/plugins/blob/master/analysis_options.yaml
11 | # - https://github.com/flutter/engine/blob/master/analysis_options.yaml
12 | #
13 | analyzer:
14 | errors:
15 | # treat missing required parameters as a warning (not a hint)
16 | missing_required_param: warning
17 | exclude:
18 | - '**.g.dart'
19 |
20 | linter:
21 | rules:
22 | - avoid_empty_else
23 | - avoid_init_to_null
24 | - avoid_return_types_on_setters
25 | - await_only_futures
26 | - camel_case_types
27 | - cancel_subscriptions
28 | - close_sinks
29 | - control_flow_in_finally
30 | - empty_constructor_bodies
31 | - empty_statements
32 | - hash_and_equals
33 | - implementation_imports
34 | - library_names
35 | - non_constant_identifier_names
36 | - package_api_docs
37 | - package_names
38 | - package_prefixed_library_names
39 | - prefer_const_constructors_in_immutables
40 | - prefer_double_quotes
41 | - prefer_final_fields
42 | - prefer_final_in_for_each
43 | - prefer_final_locals
44 | - prefer_is_not_empty
45 | - slash_for_doc_comments
46 | - test_types_in_equals
47 | - throw_in_finally
48 | - type_init_formals
49 | - unawaited_futures
50 | - unnecessary_brace_in_string_interps
51 | - unnecessary_const
52 | - unnecessary_getters_setters
53 | - unnecessary_new
54 | - unnecessary_statements
55 | - unnecessary_this
56 | - unrelated_type_equality_checks
57 | - valid_regexps
58 |
--------------------------------------------------------------------------------
/android/README.md:
--------------------------------------------------------------------------------
1 | # Timy Android
2 |
3 | ## Setting up Firebase
4 |
5 | Create a project on the Firebase console [here](https://console.firebase.google.com/)
6 |
7 | 1. To add Firebase to your app, click on the android icon or click the gear icon to go to project
8 | settings to find the android icon.
9 |
10 | 2. Register your application by filing up the form with the package name (applicationId)
11 | and the app nickname if you like.
12 | > Find Your package name which is generally the applicationId in your app-level build.gradle file
13 |
14 | 3. Download the `google-service.json` file that is generated for you. Find it and move it inside
15 | the folder `android/app/` of the project. The firebase sdk is already added to the project.
16 |
17 | 4. On the fourth step of registration, run the app to verify the configuration via the Firebase
18 | console.
19 |
20 | ## Distribution
21 |
22 | To build this application for distribution,
23 | provide a file `key.jks` containing the signing keys,
24 | and the `key.properties` with the following content:
25 |
26 | ```
27 | storePassword=.....
28 | keyPassword=.....
29 | keyAlias=key
30 | storeFile=../key.jks
31 | ```
32 |
33 | Where you set the `storePassword` and the `keyPassword`. You can also change the alias.
34 |
35 | You will need to uncomment the section `signingConfigs` in the `app/build.gradle`.
36 |
37 | And change the `signingConfig signingConfigs.debug` to `signingConfig signingConfigs.release`.
38 |
39 | You can also provide this files from your CI instead of including this in the project.
40 |
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/app/src/debug/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Timy DEV
4 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
9 |
13 |
20 |
24 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/com/example/circles_app/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.circles_app
2 |
3 | import android.os.Bundle
4 |
5 | import io.flutter.app.FlutterActivity
6 | import io.flutter.plugin.common.MethodChannel
7 | import io.flutter.plugins.GeneratedPluginRegistrant
8 |
9 | private const val UPLOAD_PLATFORM = "de.janoodle.timy/upload_platform"
10 | private const val PERMISSION = "de.janoodle.timy/permission-android"
11 | private const val THUMBNAILS = "de.janoodle.timy/thumbnails-android"
12 |
13 | class MainActivity : FlutterActivity() {
14 |
15 | override fun onCreate(savedInstanceState: Bundle?) {
16 | super.onCreate(savedInstanceState)
17 | GeneratedPluginRegistrant.registerWith(this)
18 |
19 | MethodChannel(flutterView, UPLOAD_PLATFORM).setMethodCallHandler { call, result ->
20 | when (call.method) {
21 | "uploadFiles" -> uploadFilesTask(call, applicationContext)
22 | }
23 | }
24 |
25 | MethodChannel(flutterView, PERMISSION).setMethodCallHandler { call, result ->
26 | when (call.method) {
27 | "requestPermission" -> PermissionHandler.request(call, this, result)
28 | }
29 | }
30 |
31 | MethodChannel(flutterView, THUMBNAILS).setMethodCallHandler { call, result ->
32 | when (call.method) {
33 | "getThumbnailBitmap" -> loadBitmap(call, this, result)
34 | }
35 | }
36 |
37 | }
38 |
39 | override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
40 | super.onRequestPermissionsResult(requestCode, permissions, grantResults)
41 | PermissionHandler.onRequestPermissionsResult(requestCode, permissions, grantResults)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/com/example/circles_app/PermissionHandler.kt:
--------------------------------------------------------------------------------
1 | package com.example.circles_app
2 |
3 | import android.app.Activity
4 | import android.content.pm.PackageManager
5 | import androidx.core.app.ActivityCompat
6 | import io.flutter.plugin.common.MethodCall
7 | import io.flutter.plugin.common.MethodChannel
8 |
9 | object PermissionHandler {
10 | private var result: MethodChannel.Result? = null
11 | private const val REQUEST_CODE = 1
12 |
13 | fun request(call: MethodCall, activity: Activity, result: MethodChannel.Result) {
14 | this.result = result
15 | if (ActivityCompat.checkSelfPermission(activity, call.permission()) != PackageManager.PERMISSION_GRANTED) {
16 | ActivityCompat.requestPermissions(activity, arrayOf(call.permission()), REQUEST_CODE)
17 | } else {
18 | result.success(mapOf(call.permission() to PackageManager.PERMISSION_GRANTED))
19 | }
20 | }
21 |
22 | fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
23 | if (requestCode == REQUEST_CODE) {
24 | result?.success(mapOf(permissions[0] to grantResults[0]))
25 | }
26 | }
27 | }
28 |
29 | private fun MethodCall.permission(): String {
30 | return argument("permissionType")!!
31 | }
32 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/com/example/circles_app/ThumbnailUtils.kt:
--------------------------------------------------------------------------------
1 | package com.example.circles_app
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.graphics.Bitmap
6 | import android.os.Handler
7 | import android.os.Looper
8 | import android.provider.MediaStore
9 | import io.flutter.plugin.common.MethodCall
10 | import io.flutter.plugin.common.MethodChannel
11 | import java.io.ByteArrayOutputStream
12 | import java.util.concurrent.ExecutorService
13 | import java.util.concurrent.Executors
14 |
15 | private val executor: ExecutorService = Executors.newFixedThreadPool(1)
16 |
17 | fun loadBitmap(call: MethodCall, activity: Activity, result: MethodChannel.Result) {
18 | val fileId = call.argument("fileId")
19 | val type = call.argument("type")
20 | if (fileId == null || type == null) {
21 | result.error("INVALID_ARGUMENTS", "fileId or type must not be null", null)
22 | return
23 | }
24 | executor.execute {
25 | val byteArray = getThumbnailBitmap(context = activity, fileId = fileId.toLong(), type = type)
26 | Handler(Looper.getMainLooper()).post {
27 | result.success(byteArray)
28 | }
29 | }
30 | }
31 |
32 | fun getThumbnailBitmap(context: Context, fileId: Long, type: Int): ByteArray? {
33 | val bitmap: Bitmap = when (type) {
34 | 0 -> {
35 | MediaStore.Images.Thumbnails.getThumbnail(
36 | context.contentResolver, fileId,
37 | MediaStore.Images.Thumbnails.MINI_KIND, null)
38 | }
39 | 1 -> {
40 | MediaStore.Video.Thumbnails.getThumbnail(
41 | context.contentResolver, fileId,
42 | MediaStore.Video.Thumbnails.MINI_KIND, null)
43 |
44 | }
45 | else -> null
46 | } ?: return null
47 | val stream = ByteArrayOutputStream()
48 | bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
49 | bitmap.recycle()
50 | return stream.toByteArray()
51 | }
52 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-nodpi/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/android/app/src/main/res/drawable-nodpi/splash.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | -
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/launcher_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/android/app/src/main/res/mipmap-hdpi/launcher_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/launcher_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/android/app/src/main/res/mipmap-mdpi/launcher_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/values-de/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Dateien werden hochgeladen
4 | Datei %d von %d
5 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #000000
4 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Timy
4 | Uploading files
5 | File %d of %d
6 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
--------------------------------------------------------------------------------
/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext.kotlin_version = '1.3.0'
3 | repositories {
4 | google()
5 | jcenter()
6 | maven {
7 | url 'https://maven.fabric.io/public'
8 | }
9 | }
10 |
11 | dependencies {
12 | classpath 'com.android.tools.build:gradle:3.2.1'
13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
14 | classpath 'com.google.gms:google-services:4.2.0'
15 | classpath 'io.fabric.tools:gradle:1.26.1'
16 | }
17 | }
18 |
19 | allprojects {
20 | repositories {
21 | google()
22 | jcenter()
23 | }
24 | }
25 |
26 | rootProject.buildDir = '../build'
27 | subprojects {
28 | project.buildDir = "${rootProject.buildDir}/${project.name}"
29 | }
30 | subprojects {
31 | project.evaluationDependsOn(':app')
32 | }
33 | subprojects {
34 | project.configurations.all {
35 | resolutionStrategy {
36 | force "androidx.core:core:1.0.2"
37 | force "androidx.localbroadcastmanager:localbroadcastmanager:1.0.0-rc01"
38 | }
39 | }
40 | }
41 |
42 | task clean(type: Delete) {
43 | delete rootProject.buildDir
44 | }
45 |
46 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 | android.enableR8=true
5 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Jun 23 08:50:38 CEST 2017
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip
7 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
4 |
5 | def plugins = new Properties()
6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
7 | if (pluginsFile.exists()) {
8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
9 | }
10 |
11 | plugins.each { name, path ->
12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
13 | include ":$name"
14 | project(":$name").projectDir = pluginDirectory
15 | }
16 |
--------------------------------------------------------------------------------
/android/store/feature.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/android/store/feature.png
--------------------------------------------------------------------------------
/android/store/hi-res-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/android/store/hi-res-icon.png
--------------------------------------------------------------------------------
/android/store/screenshot1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/android/store/screenshot1.png
--------------------------------------------------------------------------------
/android/store/screenshot2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/android/store/screenshot2.png
--------------------------------------------------------------------------------
/assets/graphics/avatar_no_picture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/avatar_no_picture.png
--------------------------------------------------------------------------------
/assets/graphics/calendar/calendar_today.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/calendar/calendar_today.png
--------------------------------------------------------------------------------
/assets/graphics/channel/create_new_channel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/channel/create_new_channel.png
--------------------------------------------------------------------------------
/assets/graphics/channel/details_date.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/channel/details_date.png
--------------------------------------------------------------------------------
/assets/graphics/channel/details_location.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/channel/details_location.png
--------------------------------------------------------------------------------
/assets/graphics/channel/details_members.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/channel/details_members.png
--------------------------------------------------------------------------------
/assets/graphics/channel/details_padlock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/channel/details_padlock.png
--------------------------------------------------------------------------------
/assets/graphics/channel/event_joined.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/channel/event_joined.png
--------------------------------------------------------------------------------
/assets/graphics/channel/event_open.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/channel/event_open.png
--------------------------------------------------------------------------------
/assets/graphics/channel/header_calendar_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/channel/header_calendar_icon.png
--------------------------------------------------------------------------------
/assets/graphics/channel/padlock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/channel/padlock.png
--------------------------------------------------------------------------------
/assets/graphics/channel/rsvp/rsvp_maybe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/channel/rsvp/rsvp_maybe.png
--------------------------------------------------------------------------------
/assets/graphics/channel/rsvp/rsvp_maybe_large.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/channel/rsvp/rsvp_maybe_large.png
--------------------------------------------------------------------------------
/assets/graphics/channel/rsvp/rsvp_no_large.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/channel/rsvp/rsvp_no_large.png
--------------------------------------------------------------------------------
/assets/graphics/channel/rsvp/rsvp_yes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/channel/rsvp/rsvp_yes.png
--------------------------------------------------------------------------------
/assets/graphics/channel/rsvp/rsvp_yes_large.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/channel/rsvp/rsvp_yes_large.png
--------------------------------------------------------------------------------
/assets/graphics/channel/topic_joined.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/channel/topic_joined.png
--------------------------------------------------------------------------------
/assets/graphics/channel/topic_open.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/channel/topic_open.png
--------------------------------------------------------------------------------
/assets/graphics/drawer/account.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/drawer/account.png
--------------------------------------------------------------------------------
/assets/graphics/drawer/create_topic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/drawer/create_topic.png
--------------------------------------------------------------------------------
/assets/graphics/drawer/direct_message.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/drawer/direct_message.png
--------------------------------------------------------------------------------
/assets/graphics/drawer/events.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/drawer/events.png
--------------------------------------------------------------------------------
/assets/graphics/drawer/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/drawer/settings.png
--------------------------------------------------------------------------------
/assets/graphics/icon_notification.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/icon_notification.png
--------------------------------------------------------------------------------
/assets/graphics/icon_smile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/icon_smile.png
--------------------------------------------------------------------------------
/assets/graphics/input/checkbox_active.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/input/checkbox_active.png
--------------------------------------------------------------------------------
/assets/graphics/input/checkbox_inactive.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/input/checkbox_inactive.png
--------------------------------------------------------------------------------
/assets/graphics/input/icon_add_content.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/input/icon_add_content.png
--------------------------------------------------------------------------------
/assets/graphics/input/icon_camera.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/input/icon_camera.png
--------------------------------------------------------------------------------
/assets/graphics/input/icon_pictures.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/input/icon_pictures.png
--------------------------------------------------------------------------------
/assets/graphics/menu_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/menu_icon.png
--------------------------------------------------------------------------------
/assets/graphics/menu_more_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/menu_more_icon.png
--------------------------------------------------------------------------------
/assets/graphics/update_indicator_darkgreen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/update_indicator_darkgreen.png
--------------------------------------------------------------------------------
/assets/graphics/updates_indicator.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/updates_indicator.png
--------------------------------------------------------------------------------
/assets/graphics/updates_indicator_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/updates_indicator_white.png
--------------------------------------------------------------------------------
/assets/graphics/upload/indicator_0_try_again.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/upload/indicator_0_try_again.png
--------------------------------------------------------------------------------
/assets/graphics/upload/selected.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/upload/selected.png
--------------------------------------------------------------------------------
/assets/graphics/visual_twist.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/visual_twist.png
--------------------------------------------------------------------------------
/assets/graphics/visual_twist_white_petrol.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/graphics/visual_twist_white_petrol.png
--------------------------------------------------------------------------------
/assets/icon/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/icon/icon.png
--------------------------------------------------------------------------------
/assets/placeholder/2.0x/user_image_placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/placeholder/2.0x/user_image_placeholder.png
--------------------------------------------------------------------------------
/assets/placeholder/3.0x/user_image_placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/placeholder/3.0x/user_image_placeholder.png
--------------------------------------------------------------------------------
/assets/placeholder/user_image_placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/assets/placeholder/user_image_placeholder.png
--------------------------------------------------------------------------------
/firebase/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | firebase-debug.log*
8 |
9 | # Firebase cache
10 | .firebase/
11 |
12 | # Firebase config
13 |
14 | # Uncomment this if you'd like others to create their own Firebase project.
15 | # For a team working on the same Firebase project(s), it is recommended to leave
16 | # it commented so all members can deploy to the same project(s) in .firebaserc.
17 | # .firebaserc
18 |
19 | # Runtime data
20 | pids
21 | *.pid
22 | *.seed
23 | *.pid.lock
24 |
25 | # Directory for instrumented libs generated by jscoverage/JSCover
26 | lib-cov
27 |
28 | # Coverage directory used by tools like istanbul
29 | coverage
30 |
31 | # nyc test coverage
32 | .nyc_output
33 |
34 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
35 | .grunt
36 |
37 | # Bower dependency directory (https://bower.io/)
38 | bower_components
39 |
40 | # node-waf configuration
41 | .lock-wscript
42 |
43 | # Compiled binary addons (http://nodejs.org/api/addons.html)
44 | build/Release
45 |
46 | # Dependency directories
47 | node_modules/
48 |
49 | # Optional npm cache directory
50 | .npm
51 |
52 | # Optional eslint cache
53 | .eslintcache
54 |
55 | # Optional REPL history
56 | .node_repl_history
57 |
58 | # Output of 'npm pack'
59 | *.tgz
60 |
61 | # Yarn Integrity file
62 | .yarn-integrity
63 |
64 | # dotenv environment variables file
65 | .env
66 |
--------------------------------------------------------------------------------
/firebase/README.md:
--------------------------------------------------------------------------------
1 | # Timy app on Firebase
2 |
3 | This folder contains all Firebase related code that does not belong to the client app.
4 |
5 | ## Structure
6 |
7 | - `functions` contains all the code for cloud functions
8 | - `scripts` contains local scripts for database admin and migration help
9 | - `firestore.rules` defines the access rules for Firestore by app users
10 |
--------------------------------------------------------------------------------
/firebase/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "firestore": {
3 | "rules": "firestore.rules",
4 | "indexes": "firestore.indexes.json"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/firebase/firestore.indexes.json:
--------------------------------------------------------------------------------
1 | {
2 | "indexes": [
3 | {
4 | "collectionGroup": "calendar",
5 | "queryScope": "COLLECTION",
6 | "fields": [
7 | {
8 | "fieldPath": "users",
9 | "arrayConfig": "CONTAINS"
10 | },
11 | {
12 | "fieldPath": "event_date",
13 | "order": "ASCENDING"
14 | }
15 | ]
16 | },
17 | {
18 | "collectionGroup": "channels",
19 | "queryScope": "COLLECTION",
20 | "fields": [
21 | {
22 | "fieldPath": "has_start_time",
23 | "order": "ASCENDING"
24 | },
25 | {
26 | "fieldPath": "type",
27 | "order": "ASCENDING"
28 | },
29 | {
30 | "fieldPath": "start_date",
31 | "order": "ASCENDING"
32 | }
33 | ]
34 | },
35 | {
36 | "collectionGroup": "channels",
37 | "queryScope": "COLLECTION",
38 | "fields": [
39 | {
40 | "fieldPath": "type",
41 | "order": "ASCENDING"
42 | },
43 | {
44 | "fieldPath": "start_date",
45 | "order": "ASCENDING"
46 | }
47 | ]
48 | }
49 | ],
50 | "fieldOverrides": []
51 | }
52 |
--------------------------------------------------------------------------------
/firebase/firestore.rules:
--------------------------------------------------------------------------------
1 | service cloud.firestore {
2 | match /databases/{database}/documents {
3 | function userIsMemberOfGroup(groupId) {
4 | return request.auth.uid in get(/databases/$(database)/documents/groups/$(groupId)).data.members;
5 | }
6 |
7 | match /calendar/{calendarId} {
8 | allow read, write: if request.auth.uid != null;
9 | }
10 |
11 | match /groups/{groupId} {
12 | allow read, write: if request.auth.uid in resource.data.members;
13 |
14 | // TODO: Channels should be filtered on client side by their visibility
15 | match /channels/{channelId} {
16 | allow read, write: if userIsMemberOfGroup(groupId);
17 |
18 | match /users/{userId} {
19 | allow read, create, update: if userIsMemberOfGroup(groupId);
20 | // Only allow writes on your user. Or allow author to perform writes.
21 | allow delete: if request.auth.uid == userId;
22 | }
23 |
24 | match /messages/{messageId} {
25 | allow read, write: if userIsMemberOfGroup(groupId);
26 | }
27 | }
28 | }
29 |
30 | // TODO: For security reasons we should probably move private user data in to a private sub collection.
31 | match /users/{userId} {
32 | allow update: if request.auth.uid == userId;
33 | allow read, create: if request.auth.uid != null;
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/firebase/functions/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
--------------------------------------------------------------------------------
/firebase/functions/admin.js:
--------------------------------------------------------------------------------
1 | const functions = require('firebase-functions');
2 |
3 | const admin = require('firebase-admin');
4 | admin.initializeApp(functions.config().firebase);
5 |
6 | const db = admin.firestore();
7 |
8 | module.exports = {
9 | db,
10 | admin,
11 | }
12 |
--------------------------------------------------------------------------------
/firebase/functions/channel-flagChannelUnread.js:
--------------------------------------------------------------------------------
1 | const { db } = require('./admin');
2 |
3 | /**
4 | * Sets `hasUpdates` flag for user in channel to false.
5 | * This is used to allow the channels listener on client-side
6 | * to update its list accordingly.
7 | */
8 | const flagChannelUnread = async (groupdId, channelId, userId) => {
9 | try {
10 | await db
11 | .collection("/groups/")
12 | .doc(groupdId)
13 | .collection("/channels/")
14 | .doc(channelId)
15 | .collection("/users/").doc(userId).update({
16 | hasUpdates: true
17 | });
18 | console.log("Updated has updates for user: " + userId);
19 | } catch (error) {
20 | console.error("Error writing document: ", error);
21 | }
22 | }
23 |
24 | module.exports = flagChannelUnread;
25 |
--------------------------------------------------------------------------------
/firebase/functions/channel-remove-user.js:
--------------------------------------------------------------------------------
1 | const functions = require('firebase-functions');
2 | const { updateCalendarUser } = require('./calendar-update');
3 | const { getChannel } = require('./channel-util');
4 | const updateLatestActivityForChannel = require('./channel-updatedAt');
5 | const { db } = require('./admin');
6 |
7 | /**
8 | * This function is triggered when a user leaves a channel.
9 | *
10 | * @type {CloudFunction}
11 | **/
12 |
13 | const deleteChannelUser = functions
14 | .region('europe-west1')
15 | .firestore
16 | .document('/groups/{groupId}/channels/{channelId}/users/{userId}')
17 | .onDelete(async (snap, context) => {
18 | // Remove user from calendar entry
19 | await updateCalendarUser(context.params.groupId, context.params.channelId, context.params.userId, true);
20 |
21 | // System message
22 | const channel = await getChannel(context.params.groupId, context.params.channelId);
23 |
24 | if (channel.data() == null) {
25 | return;
26 | }
27 |
28 | await updateLatestActivityForChannel(context.params.groupId, context.params.channelId, Date.now());
29 |
30 | await db.collection("/users/")
31 | .doc(context.params.userId)
32 | .get()
33 | .then(snapshot => {
34 | return db.collection(`/groups/${context.params.groupId}/channels/${context.params.channelId}/messages/`)
35 | .add({
36 | body: channel.data().type == 'EVENT' ? `${snapshot.data().name} {LEFT_EVENT}` : `${snapshot.data().name} {LEFT_CHANNEL}`,
37 | type: "SYSTEM",
38 | timestamp: `${Date.now()}`,
39 | author: { email: "system", name: "system" }
40 | });
41 | });
42 | });
43 |
44 | module.exports = deleteChannelUser;
45 |
--------------------------------------------------------------------------------
/firebase/functions/channel-remove.js:
--------------------------------------------------------------------------------
1 | const functions = require('firebase-functions');
2 | const { deleteCalendarEntry } = require('./calendar-update');
3 |
4 | const deleteChannel = functions
5 | .region('europe-west1')
6 | .firestore
7 | .document('/groups/{groupId}/channels/{channelId}')
8 | .onDelete(async (snap, context) => {
9 | return await deleteCalendarEntry(context.params.groupId, context.params.channelId);
10 | });
11 |
12 | module.exports = deleteChannel;
13 |
--------------------------------------------------------------------------------
/firebase/functions/channel-updatedAt.js:
--------------------------------------------------------------------------------
1 | const { db } = require('./admin');
2 |
3 | /**
4 | * Sets `updatedAt` for channel to last activity's timestamp.
5 | */
6 | const updateLatestActivityForChannel = async (groupId, channelId, activityDate) => {
7 | try {
8 | await db
9 | .collection("/groups/")
10 | .doc(groupId)
11 | .collection("/channels/")
12 | .doc(channelId).update({
13 | updatedAt: activityDate
14 | })
15 |
16 | } catch (error) {
17 | console.error("Error updating channel timestamp: ", error);
18 | }
19 | }
20 |
21 | module.exports = updateLatestActivityForChannel;
--------------------------------------------------------------------------------
/firebase/functions/channel-util.js:
--------------------------------------------------------------------------------
1 | const { db } = require('./admin');
2 |
3 | const getChannel = async (groupId, channelId) => {
4 | return await db
5 | .collection(`/groups/${groupId}/channels/`)
6 | .doc(channelId)
7 | .get();
8 | }
9 |
10 | const getChannelName = async (groupId, channelId) => {
11 | let channelDoc = await getChannel(groupId, channelId);
12 | return channelDoc.data().name
13 | };
14 |
15 | const sendSystemMessage = async (groupId, channelId, body) => {
16 | await db.collection(`/groups/${groupId}/channels/${channelId}/messages/`).add({
17 | body: body,
18 | type: "SYSTEM",
19 | timestamp: `${Date.now()}`,
20 | author: { email: "system", name: "system" }
21 | });
22 | };
23 |
24 |
25 | module.exports = { getChannel, getChannelName, sendSystemMessage };
26 |
--------------------------------------------------------------------------------
/firebase/functions/constants.js:
--------------------------------------------------------------------------------
1 | module.exports = Object.freeze({
2 | CHANNEL_EVENT_TYPE: 'EVENT',
3 | CHANNEL_VISIBILITY_OPEN: 'OPEN',
4 | PUSH_LOCALE_DE: 'de',
5 | RSVP_MAYBE: 'MAYBE',
6 | RSVP_YES: 'YES',
7 | EVENT_EDIT_TIME: 'EVENT_EDIT_TIME',
8 | EVENT_EDIT_VENUE: 'EVENT_EDIT_VENUE',
9 | EVENT_EDIT_DESCRIPTION: 'EVENT_EDIT_DESCRIPTION',
10 | });
--------------------------------------------------------------------------------
/firebase/functions/group-update.js:
--------------------------------------------------------------------------------
1 | const functions = require('firebase-functions');
2 | const { updateCalendarGroupName } = require('./calendar-update');
3 |
4 | const updatedGroup = functions
5 | .region('europe-west1')
6 | .firestore
7 | .document('/groups/{groupId}')
8 | .onUpdate(async (change, context) => {
9 | const groupAfter = change.after.data();
10 |
11 | // Update Calendar
12 | await updateCalendarGroupName(context.params.groupId, groupAfter.name);
13 | });
14 |
15 | module.exports = updatedGroup;
--------------------------------------------------------------------------------
/firebase/functions/group-util.js:
--------------------------------------------------------------------------------
1 | const { db } = require('./admin');
2 |
3 | const getAllGroups = async () => {
4 | let groupDocs = await db
5 | .collection('groups')
6 | .get();
7 |
8 | if (groupDocs.empty) {
9 | console.log('Could not find any groups');
10 | return null;
11 | }
12 |
13 | return groupDocs;
14 | }
15 |
16 | const getGroupName = async (groupId) => {
17 | let groupDoc = await db
18 | .collection('groups')
19 | .doc(groupId)
20 | .get();
21 |
22 | return groupDoc.data().name;
23 | }
24 |
25 | module.exports = { getGroupName, getAllGroups };
26 |
--------------------------------------------------------------------------------
/firebase/functions/index.js:
--------------------------------------------------------------------------------
1 | const updatedMessages = require('./message-update');
2 | const newMessages = require('./message-new');
3 | const newChannel = require('./channel-new');
4 | const editChannel = require('./channel-edit');
5 | const rsvpUpdate = require('./rsvp-update');
6 | const newUser = require('./user-new');
7 | const { notifyAboutUpcomingEventsToday,
8 | notifyAboutUpcomingEventsTomorrow } = require('./cron/event-notify-about-upcoming-event');
9 | const newChannelUser = require('./channel-new-user');
10 | const deleteChannelUser = require('./channel-remove-user');
11 | const updatedGroup = require('./group-update');
12 | const deleteChannel = require('./channel-remove');
13 |
14 | exports.newUser = newUser;
15 | exports.newChannelUser = newChannelUser;
16 | exports.deleteChannelUser = deleteChannelUser;
17 |
18 | exports.newChannel = newChannel;
19 | exports.editChannel = editChannel;
20 | exports.deleteChannel = deleteChannel;
21 |
22 | exports.newMessages = newMessages;
23 | exports.updatedMessages = updatedMessages;
24 |
25 | exports.rsvpUpdate = rsvpUpdate;
26 |
27 | exports.updatedGroup = updatedGroup;
28 |
29 | /// Crons make us of the scheduling APIs in firebase.
30 | /// These APIs aren't available on the free Spark Plan.
31 | /// To deploy crons you'll need to enable the `Blaze Plan` first.
32 | /*
33 | // Crons
34 | exports.notifyAboutUpcomingEventsToday = notifyAboutUpcomingEventsToday;
35 | exports.notifyAboutUpcomingEventsTomorrow = notifyAboutUpcomingEventsTomorrow;
36 | */
37 |
--------------------------------------------------------------------------------
/firebase/functions/message-get.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Create the push notification payload JSON, include the user token
3 | *
4 | * API v1 reference: https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages
5 | *
6 | */
7 | const getMessage = (token, title, body, data, apns, android) => {
8 | return {
9 | token: token,
10 | notification: {
11 | title: title,
12 | body: body
13 | },
14 | data: data,
15 | apns: apns,
16 | android: android
17 | };
18 | }
19 |
20 | module.exports = getMessage;
21 |
--------------------------------------------------------------------------------
/firebase/functions/message-new.js:
--------------------------------------------------------------------------------
1 | const functions = require('firebase-functions');
2 |
3 | const updateLatestActivityForChannel = require('./channel-updatedAt');
4 | const userGroupUpdate = require('./user-groupUpdate');
5 | const flagChannelUnread = require('./channel-flagChannelUnread');
6 | const { usersInChannel, getUser } = require('./user-util');
7 | const { sendNewMessagePush } = require('./push-send');
8 |
9 | /**
10 | * This function is triggered when a new message document is created
11 | *
12 | * @type {CloudFunction}
13 | */
14 | const newMessages = functions
15 | .region('europe-west1')
16 | .firestore
17 | .document('/groups/{groupId}/channels/{channelId}/messages/{messageId}')
18 | .onCreate(async (snap, context) => {
19 | // Obtain the newly created message
20 | const message = snap.data();
21 | const path = snap.ref.path;
22 | const body = message.body;
23 | const authorId = message.author;
24 | const attachment = message.attachment;
25 |
26 | if (message.type != "USER") {
27 | console.log('System message. Ignoring');
28 | return;
29 | }
30 |
31 | console.log(`Received ${body} with attachment: ${attachment}`);
32 |
33 | // get the list of users that joined the channel
34 | const users = await usersInChannel(context.params.groupId, context.params.channelId);
35 |
36 | // get the message author, we need their name
37 | const author = await getUser(authorId) ;
38 |
39 | // for each user in the channel, get the id
40 | for (const userSnapshot of users.docs) {
41 | let uid = userSnapshot.data().uid;
42 | // Only send notification or flag channel as unread if user is not author
43 | if (authorId !== uid) {
44 | await userGroupUpdate(context.params.groupId, uid, context.params.channelId);
45 | await flagChannelUnread(context.params.groupId, context.params.channelId, uid);
46 | await sendNewMessagePush(uid, body, attachment, context, author.data());
47 | }
48 | }
49 |
50 | await updateLatestActivityForChannel(context.params.groupId, context.params.channelId, message.timestamp);
51 | });
52 |
53 | module.exports = newMessages;
54 |
--------------------------------------------------------------------------------
/firebase/functions/message-update.js:
--------------------------------------------------------------------------------
1 | const functions = require('firebase-functions');
2 | const sendPushForNewReaction = require('./reaction-push');
3 |
4 | const updatedMessages = functions
5 | .region('europe-west1')
6 | .firestore
7 | .document('/groups/{groupId}/channels/{channelId}/messages/{messageId}')
8 | .onUpdate(async (change, context) => {
9 | const messageBefore = change.before.data();
10 | const messageAfter = change.after.data();
11 | const authorUid = messageAfter.author;
12 |
13 | await sendPushForNewReaction(messageBefore, messageAfter, authorUid, context);
14 |
15 | });
16 |
17 | module.exports = updatedMessages;
18 |
--------------------------------------------------------------------------------
/firebase/functions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "functions",
3 | "description": "Cloud Functions for Firebase",
4 | "scripts": {
5 | "lint": "./node_modules/.bin/eslint --max-warnings=0 .",
6 | "serve": "firebase serve --only functions",
7 | "shell": "firebase functions:shell",
8 | "start": "npm run shell",
9 | "deploy": "firebase deploy --only functions",
10 | "logs": "firebase functions:log"
11 | },
12 | "engines": {
13 | "node": "8"
14 | },
15 | "dependencies": {
16 | "firebase-admin": "^8.4.0",
17 | "firebase-functions": "^3.2.0"
18 | },
19 | "devDependencies": {
20 | "eslint": "^4.13.1",
21 | "eslint-plugin-promise": "^3.6.0",
22 | "firebase-functions-test": "^0.1.6"
23 | },
24 | "private": true
25 | }
26 |
--------------------------------------------------------------------------------
/firebase/functions/user-groupUpdate.js:
--------------------------------------------------------------------------------
1 | const { db, admin } = require('./admin');
2 |
3 | /**
4 | * Performs two updates:
5 | * 1. Updates / Sets `updatedGroups` user `userId` with `groupId`. This is used to build a map of unread channels for a group on the client.
6 | * 2. Adds key `groupId` with value `channelId` of all channels containing updates.
7 | * This is used to highlight that there are yet unread updates.
8 | */
9 | const userGroupUpdate = async (groupId, userId, channelId) => {
10 | try {
11 | const userDocRef = db.collection("/users/").doc(userId);
12 | await userDocRef.update({
13 | updatedGroups: admin.firestore.FieldValue.arrayUnion(groupId),
14 | [groupId]: admin.firestore.FieldValue.arrayUnion(channelId)
15 | })
16 | } catch (error) {
17 | console.error("Error updating user for group update: ", error);
18 | }
19 | }
20 |
21 | module.exports = userGroupUpdate;
--------------------------------------------------------------------------------
/firebase/functions/user-new.js:
--------------------------------------------------------------------------------
1 | const functions = require('firebase-functions');
2 | const { db } = require('./admin');
3 |
4 | const newUser = functions
5 | .region('europe-west1')
6 | .firestore
7 | .document('/users/{userId}')
8 | .onCreate(async (snapshot, context) => {
9 | const allGroups = await db.collection("/groups/")
10 | .where("members", "array-contains", context.params.userId)
11 | .get();
12 |
13 | var groupIds = [];
14 | for(const groupSnapshot of allGroups.docs) {
15 | groupIds.push(groupSnapshot.id);
16 | }
17 |
18 | // Set all "joinedGroups" according to "members" in each group
19 | snapshot.ref.update({
20 | joinedGroups : groupIds
21 | });
22 | });
23 |
24 | module.exports = newUser;
--------------------------------------------------------------------------------
/firebase/functions/user-util.js:
--------------------------------------------------------------------------------
1 | const { db } = require('./admin');
2 |
3 | const getUsersInGroup = async (groupId) => {
4 | try {
5 | return await db.collection("users")
6 | .where("joinedGroups", "array-contains", groupId)
7 | .get()
8 | } catch (error) {
9 | console.log(`Error getting users for group: ${groupId}`);
10 | }
11 | }
12 |
13 | const getUser = (uid) => {
14 | try {
15 | return db
16 | .collection('users')
17 | .doc(uid)
18 | .get();
19 | } catch (error) {
20 | console.error("Error getting user: ", error);
21 | }
22 | }
23 |
24 | const usersInChannel = async (groupId, channelId) => {
25 | return await db
26 | .collection(`/groups/${groupId}/channels/${channelId}/users`)
27 | .get();
28 | }
29 |
30 | module.exports = { getUser, usersInChannel, getUsersInGroup };
31 |
--------------------------------------------------------------------------------
/firebase/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "requires": true,
3 | "lockfileVersion": 1,
4 | "dependencies": {
5 | "moment": {
6 | "version": "2.24.0",
7 | "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
8 | "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/firebase/scripts/group-create.js:
--------------------------------------------------------------------------------
1 | const admin = require('firebase-admin');
2 |
3 | var serviceAccount = require("../your-firebase-sdk-config-file.json");
4 |
5 | admin.initializeApp({
6 | credential: admin.credential.cert(serviceAccount),
7 | databaseURL: "https://your-firebase-project.firebaseio.com"
8 | });
9 |
10 | const db = admin.firestore();
11 |
12 | (async () => {
13 | try {
14 |
15 | // Get all users of the database
16 | const users = await db.collection('/users').listDocuments();
17 | const members = [];
18 | for (let i = 0; i < users.length; i++) {
19 | members[i] = (await users[i].get()).data().uid;
20 | }
21 |
22 | // create a new 'dev' group
23 | const group = await db.collection('/groups').add({
24 | 'abbreviation': 'DV',
25 | 'color': 'fcba03',
26 | 'name': 'dev',
27 | 'members': members
28 | });
29 |
30 | // add all users to the 'dev' group
31 | for (let i = 0; i < users.length; i++) {
32 | const joinedGroups = (await users[i].get()).data().joinedGroups;
33 | joinedGroups.push(group.id);
34 | users[i].update({
35 | 'joinedGroups' : joinedGroups
36 | });
37 | }
38 |
39 | // create a general channel
40 | db.collection('/groups')
41 | .doc(group.id)
42 | .collection('/channels')
43 | .add({
44 | 'name': "general",
45 | 'type': "TOPIC",
46 | 'visibility': "OPEN"
47 | })
48 |
49 | console.log(`Group with id ${group.id} created`);
50 |
51 | } catch (e) {
52 | console.log(e);
53 | }
54 | })();
55 |
--------------------------------------------------------------------------------
/firestore-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/firestore-1.png
--------------------------------------------------------------------------------
/firestore-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/firestore-2.png
--------------------------------------------------------------------------------
/firestore-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/firestore-3.png
--------------------------------------------------------------------------------
/firestore-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/firestore-4.png
--------------------------------------------------------------------------------
/fonts/Edmondsans-Bold.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/fonts/Edmondsans-Bold.otf
--------------------------------------------------------------------------------
/fonts/Edmondsans-Medium.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/fonts/Edmondsans-Medium.otf
--------------------------------------------------------------------------------
/fonts/Edmondsans-Regular.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/fonts/Edmondsans-Regular.otf
--------------------------------------------------------------------------------
/fonts/Poppins-ExtraBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/fonts/Poppins-ExtraBold.ttf
--------------------------------------------------------------------------------
/fonts/Poppins-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/fonts/Poppins-Regular.ttf
--------------------------------------------------------------------------------
/ios/Flutter/AppFrameworkInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | App
9 | CFBundleIdentifier
10 | io.flutter.flutter.app
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | App
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1.0
23 | MinimumOSVersion
24 | 8.0
25 |
26 |
27 |
--------------------------------------------------------------------------------
/ios/Flutter/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/ios/Flutter/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/ios/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment this line to define a global platform for your project
2 | # platform :ios, '9.0'
3 |
4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true'
6 |
7 | project 'Runner', {
8 | 'Debug' => :debug,
9 | 'Profile' => :release,
10 | 'Release' => :release,
11 | }
12 |
13 | def parse_KV_file(file, separator='=')
14 | file_abs_path = File.expand_path(file)
15 | if !File.exists? file_abs_path
16 | return [];
17 | end
18 | pods_ary = []
19 | skip_line_start_symbols = ["#", "/"]
20 | File.foreach(file_abs_path) { |line|
21 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ }
22 | plugin = line.split(pattern=separator)
23 | if plugin.length == 2
24 | podname = plugin[0].strip()
25 | path = plugin[1].strip()
26 | podpath = File.expand_path("#{path}", file_abs_path)
27 | pods_ary.push({:name => podname, :path => podpath});
28 | else
29 | puts "Invalid plugin specification: #{line}"
30 | end
31 | }
32 | return pods_ary
33 | end
34 |
35 | target 'Runner' do
36 | use_frameworks!
37 |
38 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
39 | # referring to absolute paths on developers' machines.
40 | system('rm -rf .symlinks')
41 | system('mkdir -p .symlinks/plugins')
42 |
43 | # Flutter Pods
44 | generated_xcode_build_settings = parse_KV_file('./Flutter/Generated.xcconfig')
45 | if generated_xcode_build_settings.empty?
46 | puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter packages get is executed first."
47 | end
48 | generated_xcode_build_settings.map { |p|
49 | if p[:name] == 'FLUTTER_FRAMEWORK_DIR'
50 | symlink = File.join('.symlinks', 'flutter')
51 | File.symlink(File.dirname(p[:path]), symlink)
52 | pod 'Flutter', :path => File.join(symlink, File.basename(p[:path]))
53 | end
54 | }
55 |
56 | # Plugin Pods
57 | plugin_pods = parse_KV_file('../.flutter-plugins')
58 | plugin_pods.map { |p|
59 | symlink = File.join('.symlinks', 'plugins', p[:name])
60 | File.symlink(p[:path], symlink)
61 | pod p[:name], :path => File.join(symlink, 'ios')
62 | }
63 | end
64 |
65 | post_install do |installer|
66 | installer.pods_project.targets.each do |target|
67 | target.build_configurations.each do |config|
68 | config.build_settings['ENABLE_BITCODE'] = 'NO'
69 | end
70 | end
71 | end
72 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | BuildSystemType
6 | Original
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Flutter
3 |
4 | @UIApplicationMain
5 | @objc class AppDelegate: FlutterAppDelegate {
6 | override func application(
7 | _ application: UIApplication,
8 | didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?
9 | ) -> Bool {
10 |
11 | UploadService.shared.configureFlutterHandler(flutterBinaryMessenger: window.rootViewController as! FlutterBinaryMessenger)
12 | PermissionService.shared.configureFlutterHandler(flutterBinaryMessenger: window.rootViewController as! FlutterBinaryMessenger)
13 |
14 | GeneratedPluginRegistrant.register(with: self)
15 | return super.application(application, didFinishLaunchingWithOptions: launchOptions)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small-1.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small20.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x-1.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@2x-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@2x-1.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-40@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-41.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-41.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-42.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-Spotlight-42.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-iPadPro@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-iPadPro@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/timy-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/ios/Runner/Assets.xcassets/AppIcon.appiconset/timy-icon.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "splash screen.pdf"
6 | }
7 | ],
8 | "info" : {
9 | "version" : 1,
10 | "author" : "xcode"
11 | }
12 | }
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/splash screen.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/ios/Runner/Assets.xcassets/LaunchImage.imageset/splash screen.pdf
--------------------------------------------------------------------------------
/ios/Runner/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/ios/Runner/Info-dev.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | Timy | dev
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleLocalizations
16 |
17 | en
18 | de
19 |
20 | ITSAppUsesNonExemptEncryption
21 |
22 | CFBundleName
23 | Timy
24 | CFBundlePackageType
25 | APPL
26 | CFBundleShortVersionString
27 | $(FLUTTER_BUILD_NAME)
28 | CFBundleSignature
29 | ????
30 | CFBundleVersion
31 | $(FLUTTER_BUILD_NUMBER)
32 | LSRequiresIPhoneOS
33 |
34 | NSCameraUsageDescription
35 | The user can upload pictures or video as message attachments
36 | NSMicrophoneUsageDescription
37 | The user can upload videos as message attachments
38 | NSPhotoLibraryUsageDescription
39 | The user can upload pictures as message attachments
40 | UIBackgroundModes
41 |
42 | remote-notification
43 |
44 | UILaunchStoryboardName
45 | LaunchScreen
46 | UIMainStoryboardFile
47 | Main
48 | UISupportedInterfaceOrientations
49 |
50 | UIInterfaceOrientationPortrait
51 | UIInterfaceOrientationLandscapeLeft
52 | UIInterfaceOrientationLandscapeRight
53 |
54 | UISupportedInterfaceOrientations~ipad
55 |
56 | UIInterfaceOrientationPortrait
57 | UIInterfaceOrientationPortraitUpsideDown
58 | UIInterfaceOrientationLandscapeLeft
59 | UIInterfaceOrientationLandscapeRight
60 |
61 | UIViewControllerBasedStatusBarAppearance
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/ios/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleDisplayName
8 | Timy
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleLocalizations
16 |
17 | en
18 | de
19 |
20 | ITSAppUsesNonExemptEncryption
21 | CFBundleName
22 | Timy
23 | CFBundlePackageType
24 | APPL
25 | CFBundleShortVersionString
26 | $(FLUTTER_BUILD_NAME)
27 | CFBundleSignature
28 | ????
29 | CFBundleVersion
30 | $(FLUTTER_BUILD_NUMBER)
31 | LSRequiresIPhoneOS
32 |
33 | NSCameraUsageDescription
34 | The user can upload pictures or video as message attachments
35 | NSMicrophoneUsageDescription
36 | The user can upload videos as message attachments
37 | NSPhotoLibraryUsageDescription
38 | The user can upload pictures as message attachments
39 | UIBackgroundModes
40 |
41 | remote-notification
42 |
43 | UILaunchStoryboardName
44 | LaunchScreen
45 | UIMainStoryboardFile
46 | Main
47 | UISupportedInterfaceOrientations
48 |
49 | UIInterfaceOrientationPortrait
50 | UIInterfaceOrientationLandscapeLeft
51 | UIInterfaceOrientationLandscapeRight
52 |
53 | UISupportedInterfaceOrientations~ipad
54 |
55 | UIInterfaceOrientationPortrait
56 | UIInterfaceOrientationPortraitUpsideDown
57 | UIInterfaceOrientationLandscapeLeft
58 | UIInterfaceOrientationLandscapeRight
59 |
60 | UIViewControllerBasedStatusBarAppearance
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/ios/Runner/Runner-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #import "GeneratedPluginRegistrant.h"
--------------------------------------------------------------------------------
/ios/Runner/Runner-dev.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | aps-environment
6 | development
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner/Runner.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | aps-environment
6 | production
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner/de.lproj/InfoPlist.strings:
--------------------------------------------------------------------------------
1 | NSCameraUsageDescription = "Für Bild- und Videonachrichten benötigten wir Zugriff auf deine Kamera";
2 | NSMicrophoneUsageDescription = "Dein Mikrofon wird für Videonachrichten benötigt";
3 | NSPhotoLibraryUsageDescription = "Für Bildnachrichten benötigen wir Zugriff auf deine Fotos";
4 |
--------------------------------------------------------------------------------
/ios/Runner/de.lproj/LaunchScreen.strings:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/ios/Runner/de.lproj/Main.strings:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/ios/Runner/en.lproj/InfoPlist.strings:
--------------------------------------------------------------------------------
1 | NSCameraUsageDescription = "The user can upload pictures or video as message attachments";
2 | NSMicrophoneUsageDescription = "The user can upload videos as message attachments";
3 | NSPhotoLibraryUsageDescription = "The user can upload pictures as message attachments";
4 |
--------------------------------------------------------------------------------
/lib/cupertinoLocalizationDelegate.dart:
--------------------------------------------------------------------------------
1 | import "package:flutter/cupertino.dart";
2 |
3 | // This delegate fixes an issue which caused alerts on iOS to fail.
4 | // https://github.com/flutter/flutter/issues/23047
5 |
6 | class FallbackCupertinoLocalisationsDelegate
7 | extends LocalizationsDelegate {
8 | const FallbackCupertinoLocalisationsDelegate();
9 |
10 | @override
11 | bool isSupported(Locale locale) => true;
12 |
13 | @override
14 | Future load(Locale locale) =>
15 | DefaultCupertinoLocalizations.load(locale);
16 |
17 | @override
18 | bool shouldReload(FallbackCupertinoLocalisationsDelegate old) => false;
19 | }
20 |
--------------------------------------------------------------------------------
/lib/data/calendar_repository.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/data/firestore_paths.dart";
2 | import "package:circles_app/model/calendar_entry.dart";
3 | import "package:cloud_firestore/cloud_firestore.dart";
4 |
5 | class CalendarRepository {
6 | final Firestore _firestore;
7 | const CalendarRepository(this._firestore);
8 |
9 | Future> getCalendarEntries(String userId) async {
10 | final snapshot = await _firestore
11 | .collection(FirestorePaths.PATH_CALENDAR)
12 | .where(_Constants.USERS, arrayContains: userId)
13 | .orderBy(_Constants.EVENTDATE, descending: false)
14 | .limit(100)
15 | .getDocuments();
16 |
17 | return snapshot.documents.map((d) => _fromDoc(d)).toList();
18 | }
19 |
20 | static CalendarEntry _fromDoc(DocumentSnapshot doc) {
21 | return CalendarEntry((calendarEntry) => calendarEntry
22 | ..channelId = doc[_Constants.CHANNELID]
23 | ..groupId = doc[_Constants.GROUPID]
24 | ..groupName = doc[_Constants.GROUPNAME]
25 | ..channelName = doc[_Constants.CHANNELNAME]
26 | ..eventDate = doc[_Constants.EVENTDATE].toDate()
27 | ..hasStartTime = doc[_Constants.HASSTARTTIME] != null
28 | ? doc[_Constants.HASSTARTTIME]
29 | : false);
30 | }
31 | }
32 |
33 | class _Constants {
34 | static const String CHANNELID = "channel_id";
35 | static const String GROUPID = "group_id";
36 | static const String GROUPNAME = "group_name";
37 | static const String CHANNELNAME = "channel_name";
38 | static const String EVENTDATE = "event_date";
39 | static const String HASSTARTTIME = "has_start_time";
40 | static const String USERS = "users";
41 | }
42 |
--------------------------------------------------------------------------------
/lib/data/file_repository.dart:
--------------------------------------------------------------------------------
1 | import "dart:io";
2 |
3 | import "package:firebase_storage/firebase_storage.dart";
4 |
5 | class FileRepository {
6 | final FirebaseStorage _firebaseStorage;
7 |
8 | FileRepository(this._firebaseStorage);
9 |
10 | Future uploadFile(File file) async {
11 | final String fileName = DateTime.now().millisecondsSinceEpoch.toString();
12 | final StorageReference reference = _firebaseStorage.ref().child(fileName);
13 | final StorageUploadTask uploadTask = reference.putFile(file);
14 | final StorageTaskSnapshot storageTaskSnapshot = await uploadTask.onComplete;
15 | return storageTaskSnapshot.ref.getDownloadURL();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/lib/data/firestore_paths.dart:
--------------------------------------------------------------------------------
1 | class FirestorePaths {
2 | static const PATH_GROUPS = "groups";
3 | static const PATH_CHANNELS = "channels";
4 | static const PATH_MESSAGES = "messages";
5 | static const PATH_USERS = "users";
6 | static const PATH_CALENDAR = "calendar";
7 |
8 | static String groupPath(String groupId) {
9 | return "$PATH_GROUPS/$groupId";
10 | }
11 |
12 | static String channelsPath(String groupId) {
13 | return "$PATH_GROUPS/$groupId/$PATH_CHANNELS";
14 | }
15 |
16 | static String channelPath(String groupId, String channelId) {
17 | return "$PATH_GROUPS/$groupId/$PATH_CHANNELS/$channelId";
18 | }
19 |
20 | static String channelUsersPath(String groupId, String channelId) {
21 | return "$PATH_GROUPS/$groupId/$PATH_CHANNELS/$channelId/$PATH_USERS";
22 | }
23 |
24 | static String messagesPath(String groupId, String channelId) {
25 | return "$PATH_GROUPS/$groupId/$PATH_CHANNELS/$channelId/$PATH_MESSAGES";
26 | }
27 |
28 | static String messagePath(
29 | String groupId,
30 | String channelId,
31 | String messageId,
32 | ) {
33 | return "$PATH_GROUPS/$groupId/$PATH_CHANNELS/$channelId/$PATH_MESSAGES/$messageId";
34 | }
35 |
36 | static String userPath(String userId) {
37 | return "$PATH_USERS/$userId";
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/lib/data/group_repository.dart:
--------------------------------------------------------------------------------
1 | import "dart:core";
2 | import "package:circles_app/data/firestore_paths.dart";
3 | import "package:circles_app/model/group.dart";
4 | import "package:cloud_firestore/cloud_firestore.dart";
5 |
6 | class GroupRepository {
7 | static const String NAME = "name";
8 | static const String ABBREVIATION = "abbreviation";
9 | static const String IMAGE = "image";
10 | static const String COLOR = "color";
11 | static const String MEMBERS = "members";
12 |
13 | final Firestore firestore;
14 |
15 | const GroupRepository(this.firestore);
16 |
17 | Future getGroup(String id) async {
18 | final doc = await firestore.document(FirestorePaths.groupPath(id)).get();
19 | return fromDoc(doc);
20 | }
21 |
22 | Stream> getGroupStream(userId) {
23 | return firestore
24 | .collection(FirestorePaths.PATH_GROUPS)
25 | .where(MEMBERS, arrayContains: userId)
26 | .snapshots()
27 | .map((snapShot) {
28 | return snapShot.documents.map((doc) => fromDoc(doc)).toList();
29 | });
30 | }
31 |
32 | static Group fromDoc(DocumentSnapshot doc) {
33 | return Group((c) => c
34 | ..id = doc.documentID
35 | ..name = doc[NAME]
36 | ..image = doc[IMAGE]
37 | ..abbreviation = doc[ABBREVIATION]
38 | ..hexColor = doc[COLOR]);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/lib/domain/redux/app_actions.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/model/group.dart";
2 | import "package:meta/meta.dart";
3 |
4 | /// Actions are payloads of information that send data from your application to
5 | /// your store. They are the only source of information for the store.
6 | ///
7 | /// They are PODOs (Plain Old Dart Objects).
8 | ///
9 | class ConnectToDataSource {
10 | @override
11 | String toString() {
12 | return "ConnectToDataSource{}";
13 | }
14 | }
15 |
16 | @immutable
17 | class OnGroupsLoaded {
18 | final List groups;
19 |
20 | const OnGroupsLoaded(this.groups);
21 |
22 | @override
23 | String toString() {
24 | return "OnGroupsLoaded{groups: $groups}";
25 | }
26 | }
27 |
28 | @immutable
29 | class SelectGroup {
30 | final String groupId;
31 |
32 | const SelectGroup(this.groupId);
33 |
34 | @override
35 | String toString() {
36 | return "SelectGroup{groupId: $groupId}";
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/lib/domain/redux/app_reducer.dart:
--------------------------------------------------------------------------------
1 | import "package:built_collection/built_collection.dart";
2 | import "package:circles_app/domain/redux/app_actions.dart";
3 | import "package:circles_app/domain/redux/app_state.dart";
4 | import "package:circles_app/domain/redux/authentication/auth_reducer.dart";
5 | import "package:circles_app/domain/redux/calendar/calendar_reducer.dart";
6 | import "package:circles_app/domain/redux/channel/channel_reducer.dart";
7 | import "package:circles_app/domain/redux/ui/ui_reducer.dart";
8 | import "package:circles_app/model/channel_state.dart";
9 | import "package:circles_app/domain/redux/message/message_reducer.dart";
10 | import "package:circles_app/domain/redux/push/push_reducer.dart";
11 | import "package:circles_app/domain/redux/user/user_reducer.dart";
12 | import "package:circles_app/model/group.dart";
13 | import "package:redux/redux.dart";
14 |
15 | /// Reducers specify how the application"s state changes in response to actions
16 | /// sent to the store.
17 | ///
18 | /// Each reducer returns a new [AppState].
19 | ///
20 | final appReducer = combineReducers([
21 | TypedReducer(_onGroupsLoaded),
22 | TypedReducer(_onSelectGroup),
23 | ...authReducers,
24 | ...userReducers,
25 | ...calendarReducer,
26 | ...channelReducers,
27 | ...messageReducers,
28 | ...pushReducers,
29 | ...uiReducers,
30 | ]);
31 |
32 | AppState _onGroupsLoaded(AppState state, OnGroupsLoaded action) {
33 | if (action.groups.isNotEmpty) {
34 | final selectedGroup = state.selectedGroupId;
35 | final Map groups = Map.fromIterable(
36 | action.groups,
37 | key: (item) => item.id,
38 | value: (item) => item,
39 | );
40 | return state.rebuild((a) => a
41 | ..selectedGroupId = selectedGroup
42 | ..groups = MapBuilder(groups));
43 | } else {
44 | return state.rebuild((a) => a
45 | ..channelState = ChannelState.init().toBuilder()
46 | ..groups = MapBuilder());
47 | }
48 | }
49 |
50 | AppState _onSelectGroup(AppState state, SelectGroup action) {
51 | return state.rebuild((a) => a..selectedGroupId = action.groupId);
52 | }
53 |
--------------------------------------------------------------------------------
/lib/domain/redux/app_selector.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/domain/redux/app_state.dart";
2 | import "package:circles_app/model/channel.dart";
3 |
4 | Channel getSelectedChannel(AppState state) {
5 | if (state.selectedGroupId == null) return null;
6 | if (state.channelState.selectedChannel == null) return null;
7 | return state.groups[state.selectedGroupId]
8 | .channels[state.channelState.selectedChannel];
9 | }
10 |
--------------------------------------------------------------------------------
/lib/domain/redux/app_state.dart:
--------------------------------------------------------------------------------
1 | import "package:built_collection/built_collection.dart";
2 | import "package:built_value/built_value.dart";
3 | import "package:circles_app/domain/redux/ui/ui_state.dart";
4 | import "package:circles_app/model/calendar_entry.dart";
5 | import "package:circles_app/model/channel_state.dart";
6 | import "package:circles_app/model/group.dart";
7 | import "package:circles_app/model/in_app_notification.dart";
8 | import "package:circles_app/model/message.dart";
9 | import "package:circles_app/model/user.dart";
10 |
11 | // ignore: prefer_double_quotes
12 | part 'app_state.g.dart';
13 |
14 | /// This class holds the whole application state.
15 | /// Which can include:
16 | /// - user calendar
17 | /// - current user profile
18 | /// - joined channels
19 | /// - received messages
20 | /// - etc.
21 | ///
22 | abstract class AppState implements Built {
23 | BuiltList get userCalendar;
24 |
25 | BuiltMap get groups;
26 |
27 | @nullable
28 | String get selectedGroupId;
29 |
30 | @nullable
31 | User get user;
32 |
33 | BuiltList get groupUsers;
34 |
35 | ChannelState get channelState;
36 |
37 | BuiltList get messagesOnScreen;
38 |
39 | @nullable
40 | String get fcmToken;
41 |
42 | @nullable
43 | InAppNotification get inAppNotification;
44 |
45 | UiState get uiState;
46 |
47 | AppState._();
48 |
49 | factory AppState([void Function(AppStateBuilder) updates]) = _$AppState;
50 |
51 | factory AppState.init() => AppState((a) => a
52 | ..groups = MapBuilder()
53 | ..channelState = ChannelState.init().toBuilder()
54 | ..messagesOnScreen = ListBuilder()
55 | ..groupUsers = ListBuilder()
56 | ..userCalendar = ListBuilder()
57 | ..uiState = UiState().toBuilder());
58 |
59 | AppState clear() {
60 | // keep the temporal fcm token even when clearing state
61 | // so it can be set again on login.
62 | //
63 | // Add here anything else that also needs to be carried over.
64 | return AppState.init().rebuild((s) => s..fcmToken = fcmToken);
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/lib/domain/redux/attachment/attachment_actions.dart:
--------------------------------------------------------------------------------
1 | import "dart:io";
2 |
3 | import "package:circles_app/model/user.dart";
4 | import "package:flutter/cupertino.dart";
5 | import "package:meta/meta.dart";
6 |
7 | @immutable
8 | class NewMessageWithMultipleFilesAction {
9 | final List fileIdentifiers; // File paths in case of Android, localIdentifier in case of iOS multiselect & path in case of camera image
10 | final bool isPath;
11 | const NewMessageWithMultipleFilesAction(this.fileIdentifiers, this.isPath);
12 | }
13 |
14 | @immutable
15 | class ChangeAvatarAction {
16 | final File file;
17 | final User user;
18 |
19 | const ChangeAvatarAction({
20 | @required this.file,
21 | @required this.user,
22 | });
23 | }
24 |
--------------------------------------------------------------------------------
/lib/domain/redux/attachment/image_processor.dart:
--------------------------------------------------------------------------------
1 | import "dart:math";
2 |
3 | import "package:flutter_native_image/flutter_native_image.dart";
4 | import "dart:io";
5 |
6 | const avatarSize = 200;
7 |
8 | class ImageProcessor {
9 | Future cropAndResizeAvatar(File file) async {
10 | final ImageProperties properties =
11 | await FlutterNativeImage.getImageProperties(file.path);
12 | final squareSize = min(properties.width, properties.height);
13 | final originX = ((properties.width / 2) - (squareSize / 2)).toInt();
14 | final originY = ((properties.height / 2) - (squareSize / 2)).toInt();
15 | final cropped = await FlutterNativeImage.cropImage(
16 | file.path, originX, originY, squareSize, squareSize);
17 | return await FlutterNativeImage.compressImage(cropped.path,
18 | quality: 95, targetWidth: avatarSize, targetHeight: avatarSize);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/lib/domain/redux/authentication/auth_actions.dart:
--------------------------------------------------------------------------------
1 | import "dart:async";
2 | import "package:circles_app/model/user.dart";
3 | import "package:meta/meta.dart";
4 |
5 | // Authentication
6 | class VerifyAuthenticationState {}
7 |
8 | class LogIn {
9 | final String email;
10 | final String password;
11 | final Completer completer;
12 |
13 | LogIn({this.email, this.password, Completer completer})
14 | : completer = completer ?? Completer();
15 | }
16 |
17 | @immutable
18 | class OnAuthenticated {
19 | final User user;
20 |
21 | const OnAuthenticated({@required this.user});
22 |
23 | @override
24 | String toString() {
25 | return "OnAuthenticated{user: $user}";
26 | }
27 | }
28 |
29 | class LogOutAction {}
30 |
31 | class OnLogoutSuccess {
32 | OnLogoutSuccess();
33 |
34 | @override
35 | String toString() {
36 | return "LogOut{user: null}";
37 | }
38 | }
39 |
40 | class OnLogoutFail {
41 | final dynamic error;
42 |
43 | OnLogoutFail(this.error);
44 |
45 | @override
46 | String toString() {
47 | return "OnLogoutFail{There was an error logging in: $error}";
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/lib/domain/redux/authentication/auth_reducer.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/domain/redux/app_state.dart";
2 | import "package:circles_app/domain/redux/authentication/auth_actions.dart";
3 | import "package:redux/redux.dart";
4 |
5 | final authReducers = [
6 | TypedReducer(_onAuthenticated),
7 | TypedReducer(_onLogout),
8 | ];
9 |
10 | AppState _onAuthenticated(AppState state, OnAuthenticated action) {
11 | return state.rebuild((a) => a..user = action.user.toBuilder());
12 | }
13 |
14 | AppState _onLogout(AppState state, OnLogoutSuccess action) {
15 | return state.clear();
16 | }
17 |
--------------------------------------------------------------------------------
/lib/domain/redux/calendar/calendar_actions.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/model/calendar_entry.dart";
2 | import "package:flutter/foundation.dart";
3 |
4 | @immutable
5 | class CalendarUpdatedAction {
6 | final List calendarEntries;
7 |
8 | const CalendarUpdatedAction({this.calendarEntries});
9 |
10 | @override
11 | String toString() {
12 | return "CalendarUpdatedAction{calendarEntries: $calendarEntries}";
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/lib/domain/redux/calendar/calendar_middleware.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/data/calendar_repository.dart";
2 | import "package:circles_app/domain/redux/app_state.dart";
3 | import "package:circles_app/domain/redux/calendar/calendar_actions.dart";
4 | import "package:circles_app/domain/redux/channel/channel_actions.dart";
5 | import "package:redux/redux.dart";
6 |
7 | List> createCalendarMiddleware(
8 | CalendarRepository calendarRepository,
9 | ) {
10 | return [
11 | TypedMiddleware(_loadCalendar(
12 | calendarRepository,
13 | )),
14 | ];
15 | }
16 |
17 | /// Load calendarEntries when channel list updates
18 | void Function(
19 | Store store,
20 | OnChannelsLoaded action,
21 | NextDispatcher next,
22 | ) _loadCalendar(
23 | CalendarRepository calendarRepository,
24 | ) {
25 | return (store, action, next) async {
26 | next(action);
27 |
28 | final calendarEntries =
29 | await calendarRepository.getCalendarEntries(store.state.user.uid);
30 |
31 | store.dispatch(
32 | CalendarUpdatedAction(calendarEntries: calendarEntries),
33 | );
34 | };
35 | }
36 |
--------------------------------------------------------------------------------
/lib/domain/redux/calendar/calendar_reducer.dart:
--------------------------------------------------------------------------------
1 | import "package:built_collection/built_collection.dart";
2 | import "package:circles_app/domain/redux/app_state.dart";
3 | import "package:circles_app/domain/redux/calendar/calendar_actions.dart";
4 | import "package:redux/redux.dart";
5 |
6 | final calendarReducer = [
7 | TypedReducer(_onCalendarUpdate),
8 | ];
9 |
10 | AppState _onCalendarUpdate(AppState state, CalendarUpdatedAction action) {
11 | return state.rebuild((appState) =>
12 | appState..userCalendar = ListBuilder(action.calendarEntries));
13 | }
14 |
--------------------------------------------------------------------------------
/lib/domain/redux/message/message_actions.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/model/message.dart";
2 | import "package:meta/meta.dart";
3 |
4 | @immutable
5 | class SendMessage {
6 | final String message;
7 |
8 | const SendMessage(
9 | this.message,
10 | );
11 |
12 | @override
13 | String toString() {
14 | return "SendMessage{message: $message}";
15 | }
16 | }
17 |
18 | @immutable
19 | class UpdateAllMessages {
20 | final List data;
21 |
22 | const UpdateAllMessages(this.data);
23 | }
24 |
25 | @immutable
26 | class DeleteMessage {
27 | final String messageId;
28 |
29 | const DeleteMessage(this.messageId);
30 | }
31 |
32 | @immutable
33 | class EmojiReaction {
34 | final String emoji;
35 | final String messageId;
36 |
37 | const EmojiReaction(this.messageId, this.emoji);
38 | }
39 |
40 | @immutable
41 | class RemoveEmojiReaction {
42 | final String messageId;
43 |
44 | const RemoveEmojiReaction(this.messageId);
45 | }
46 |
--------------------------------------------------------------------------------
/lib/domain/redux/message/message_reducer.dart:
--------------------------------------------------------------------------------
1 | import "package:built_collection/built_collection.dart";
2 | import "package:circles_app/domain/redux/app_state.dart";
3 | import "package:circles_app/domain/redux/message/message_actions.dart";
4 | import "package:redux/redux.dart";
5 |
6 | final messageReducers = [
7 | TypedReducer(_onMessageUpdated),
8 | ];
9 |
10 | AppState _onMessageUpdated(AppState state, UpdateAllMessages action) {
11 | return state.rebuild((a) => a..messagesOnScreen = ListBuilder(action.data));
12 | }
13 |
--------------------------------------------------------------------------------
/lib/domain/redux/push/push_actions.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/model/in_app_notification.dart";
2 |
3 | class UpdateUserTokenAction {
4 | final String token;
5 |
6 | UpdateUserTokenAction(this.token);
7 | }
8 |
9 | class OnPushNotificationOpenAction {
10 | final Map message;
11 |
12 | OnPushNotificationOpenAction(this.message);
13 | }
14 |
15 | class OnPushNotificationReceivedAction {
16 | final Map message;
17 |
18 | OnPushNotificationReceivedAction(this.message);
19 | }
20 |
21 | class ShowPushNotificationAction {
22 | InAppNotification inAppNotification;
23 |
24 | ShowPushNotificationAction(this.inAppNotification);
25 |
26 | @override
27 | bool operator ==(Object other) =>
28 | identical(this, other) ||
29 | other is ShowPushNotificationAction &&
30 | runtimeType == other.runtimeType &&
31 | inAppNotification == other.inAppNotification;
32 |
33 | @override
34 | int get hashCode => inAppNotification.hashCode;
35 | }
36 |
37 | class OnPushNotificationDismissedAction {}
38 |
--------------------------------------------------------------------------------
/lib/domain/redux/push/push_reducer.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/domain/redux/app_state.dart";
2 | import "package:circles_app/domain/redux/push/push_actions.dart";
3 | import "package:redux/redux.dart";
4 |
5 | final pushReducers = [
6 | TypedReducer(_updateUserAction),
7 | TypedReducer(
8 | _showPushNotificationAction),
9 | TypedReducer(
10 | _onPushNotificationDismissed),
11 | ];
12 |
13 | AppState _updateUserAction(AppState state, UpdateUserTokenAction action) {
14 | return state.rebuild((s) => s..fcmToken = action.token);
15 | }
16 |
17 | AppState _showPushNotificationAction(
18 | AppState state, ShowPushNotificationAction action) {
19 | return state.rebuild(
20 | (s) => s..inAppNotification = action.inAppNotification.toBuilder());
21 | }
22 |
23 | AppState _onPushNotificationDismissed(
24 | AppState state,
25 | OnPushNotificationDismissedAction action,
26 | ) {
27 | return state.rebuild((s) => s..inAppNotification = null);
28 | }
29 |
--------------------------------------------------------------------------------
/lib/domain/redux/stream_subscriptions.dart:
--------------------------------------------------------------------------------
1 | import "dart:async";
2 |
3 | import "package:circles_app/model/channel.dart";
4 | import "package:circles_app/model/group.dart";
5 | import "package:circles_app/model/message.dart";
6 | import "package:circles_app/model/user.dart";
7 |
8 | // App user
9 | StreamSubscription userUpdateSubscription;
10 | // List of user's groups
11 | StreamSubscription> groupsSubscription;
12 | // List of users of the current selected group
13 | StreamSubscription> groupUsersSubscription;
14 | // List of channels of the current selected group
15 | StreamSubscription> listOfChannelsSubscription;
16 | // Selected channel
17 | StreamSubscription selectedChannelSubscription;
18 | // Messages from selected channel
19 | StreamSubscription> messagesSubscription;
20 |
21 | /// Cancels all active subscriptions
22 | ///
23 | /// Called on successful logout.
24 | cancelAllSubscriptions() {
25 | userUpdateSubscription?.cancel();
26 | groupsSubscription?.cancel();
27 | groupUsersSubscription?.cancel();
28 | listOfChannelsSubscription?.cancel();
29 | selectedChannelSubscription?.cancel();
30 | messagesSubscription?.cancel();
31 | }
--------------------------------------------------------------------------------
/lib/domain/redux/ui/ui_actions.dart:
--------------------------------------------------------------------------------
1 | import "package:meta/meta.dart";
2 |
3 | class UpdatedChatDraftAction {
4 | final String text;
5 | final String groupId;
6 | final String channelId;
7 |
8 | const UpdatedChatDraftAction({
9 | @required this.text,
10 | @required this.groupId,
11 | @required this.channelId,
12 | });
13 |
14 | @override
15 | String toString() {
16 | return "UpdatedChatDraftAction{text: $text, groupId: $groupId, channelId: $channelId}";
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/lib/domain/redux/ui/ui_reducer.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/domain/redux/app_state.dart";
2 | import "package:circles_app/domain/redux/ui/ui_actions.dart";
3 | import "package:circles_app/domain/redux/ui/ui_state_selector.dart";
4 | import "package:circles_app/util/logger.dart";
5 | import "package:redux/redux.dart";
6 |
7 | final uiReducers = [
8 | TypedReducer(_onUpdatedChatDraft),
9 | ];
10 |
11 | AppState _onUpdatedChatDraft(AppState state, UpdatedChatDraftAction action) {
12 | Logger.d(action.toString());
13 | final out = state.rebuild(
14 | (s) => s
15 | ..uiState.update(
16 | (u) => updateInputDraft(
17 | state: u,
18 | groupId: action.groupId,
19 | channelId: action.channelId,
20 | value: action.text,
21 | ),
22 | ),
23 | );
24 | Logger.d(out.uiState.toString());
25 | return out;
26 | }
27 |
--------------------------------------------------------------------------------
/lib/domain/redux/ui/ui_state.dart:
--------------------------------------------------------------------------------
1 | import "package:built_collection/built_collection.dart";
2 | import "package:built_value/built_value.dart";
3 |
4 | // ignore: prefer_double_quotes
5 | part 'ui_state.g.dart';
6 |
7 | ///
8 | /// Store different UI related data (last selected channel, channel input text, etc.)
9 | ///
10 | ///
11 | abstract class UiState implements Built {
12 |
13 |
14 | // Group UI state per group id
15 | BuiltMap get groupUiState;
16 |
17 | UiState._();
18 | factory UiState([void Function(UiStateBuilder) updates]) = _$UiState;
19 | }
20 |
21 | ///
22 | /// Store UI related data per group
23 | ///
24 | abstract class GroupUiState implements Built {
25 | // When a user changes groups, pick the last selected channel if present
26 | @nullable
27 | String get lastSelectedChannel;
28 |
29 | // Channel UI state per channel id
30 | BuiltMap get channelUiState;
31 |
32 | GroupUiState._();
33 | factory GroupUiState([void Function(GroupUiStateBuilder) updates]) = _$GroupUiState;
34 | }
35 |
36 | ///
37 | /// Store UI related data per channel
38 | ///
39 | abstract class ChannelUiState implements Built {
40 |
41 | @nullable
42 | String get inputDraft;
43 |
44 | ChannelUiState._();
45 | factory ChannelUiState([void Function(ChannelUiStateBuilder) updates]) = _$ChannelUiState;
46 | }
47 |
--------------------------------------------------------------------------------
/lib/domain/redux/user/user_actions.dart:
--------------------------------------------------------------------------------
1 | import "dart:async";
2 |
3 | import "package:circles_app/model/user.dart";
4 | import "package:meta/meta.dart";
5 |
6 | @immutable
7 | class UsersUpdateAction {
8 | final List users;
9 |
10 | const UsersUpdateAction(this.users);
11 | }
12 |
13 | @immutable
14 | class OnUserUpdateAction {
15 | final User user;
16 |
17 | const OnUserUpdateAction(this.user);
18 | }
19 |
20 | @immutable
21 | class UpdateUserLocaleAction {
22 | final String locale;
23 |
24 | const UpdateUserLocaleAction(this.locale);
25 | }
26 |
27 | @immutable
28 | class UpdateUserAction {
29 | final User user;
30 | final Completer completer;
31 |
32 | const UpdateUserAction(this.user, this.completer);
33 | }
34 |
--------------------------------------------------------------------------------
/lib/domain/redux/user/user_reducer.dart:
--------------------------------------------------------------------------------
1 | import "package:built_collection/built_collection.dart";
2 | import "package:circles_app/domain/redux/app_state.dart";
3 | import "package:circles_app/domain/redux/user/user_actions.dart";
4 | import "package:redux/redux.dart";
5 |
6 | final userReducers = [
7 | TypedReducer(_onUsersUpdate),
8 | TypedReducer(_onUserUpdate),
9 | ];
10 |
11 | AppState _onUserUpdate(AppState state, OnUserUpdateAction action) {
12 | return state.rebuild((a) => a
13 | // Update the app user
14 | ..user = action.user.toBuilder()
15 | // Update the user in the groupUsers
16 | ..groupUsers.removeWhere((u) => u.uid == action.user.uid)
17 | ..groupUsers.add(action.user));
18 | }
19 |
20 | AppState _onUsersUpdate(AppState state, UsersUpdateAction action) {
21 | return state.rebuild((a) => a..groupUsers = ListBuilder(action.users));
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/lib/main.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/circles_app.dart";
2 | import "package:circles_app/util/logger.dart";
3 | import "package:flutter/material.dart";
4 |
5 | void main() {
6 | configureLogger();
7 | runApp(CirclesApp());
8 | }
9 |
--------------------------------------------------------------------------------
/lib/model/calendar_entry.dart:
--------------------------------------------------------------------------------
1 | import "package:built_value/built_value.dart";
2 |
3 | // ignore: prefer_double_quotes
4 | part 'calendar_entry.g.dart';
5 |
6 | abstract class CalendarEntry implements Built {
7 | String get channelId;
8 |
9 | String get channelName;
10 |
11 | String get groupId;
12 |
13 | String get groupName;
14 |
15 | DateTime get eventDate;
16 |
17 | bool get hasStartTime;
18 |
19 | CalendarEntry._();
20 |
21 | factory CalendarEntry([void Function(CalendarEntryBuilder) updates]) = _$CalendarEntry;
22 | }
23 |
--------------------------------------------------------------------------------
/lib/model/channel_state.dart:
--------------------------------------------------------------------------------
1 | import "package:built_value/built_value.dart";
2 |
3 | // ignore: prefer_double_quotes
4 | part 'channel_state.g.dart';
5 |
6 | abstract class ChannelState
7 | implements Built {
8 | @nullable
9 | String get selectedChannel;
10 |
11 | bool get joinChannelFailed;
12 |
13 | ChannelState._();
14 |
15 | factory ChannelState([void Function(ChannelStateBuilder) updates]) =
16 | _$ChannelState;
17 |
18 | factory ChannelState.init() => ChannelState((c) => c
19 | ..selectedChannel = null
20 | ..joinChannelFailed = false);
21 | }
22 |
--------------------------------------------------------------------------------
/lib/model/group.dart:
--------------------------------------------------------------------------------
1 | import "package:built_collection/built_collection.dart";
2 | import "package:built_value/built_value.dart";
3 | import "package:circles_app/model/channel.dart";
4 |
5 | // ignore: prefer_double_quotes
6 | part 'group.g.dart';
7 |
8 | abstract class Group implements Built {
9 | String get id;
10 |
11 | String get name;
12 |
13 | String get hexColor;
14 |
15 | @nullable
16 | String get image;
17 |
18 | String get abbreviation;
19 |
20 | BuiltMap get channels;
21 |
22 | Group._();
23 |
24 | factory Group([void Function(GroupBuilder) updates]) = _$Group;
25 | }
26 |
--------------------------------------------------------------------------------
/lib/model/in_app_notification.dart:
--------------------------------------------------------------------------------
1 | import "package:built_value/built_value.dart";
2 | import "package:circles_app/model/channel.dart";
3 |
4 | // ignore: prefer_double_quotes
5 | part 'in_app_notification.g.dart';
6 |
7 | abstract class InAppNotification implements Built {
8 |
9 | String get groupId;
10 |
11 | String get groupName;
12 |
13 | String get userName;
14 |
15 | String get message;
16 |
17 | Channel get channel;
18 |
19 | InAppNotification._();
20 | factory InAppNotification([void Function(InAppNotificationBuilder) updates]) = _$InAppNotification;
21 | }
--------------------------------------------------------------------------------
/lib/model/reaction.dart:
--------------------------------------------------------------------------------
1 |
2 | import "package:built_value/built_value.dart";
3 |
4 | // ignore: prefer_double_quotes
5 | part 'reaction.g.dart';
6 |
7 | abstract class Reaction implements Built {
8 |
9 | String get emoji;
10 |
11 | String get userId;
12 |
13 | String get userName;
14 |
15 | DateTime get timestamp;
16 |
17 | Reaction._();
18 | factory Reaction([void Function(ReactionBuilder) updates]) = _$Reaction;
19 | }
--------------------------------------------------------------------------------
/lib/model/user.dart:
--------------------------------------------------------------------------------
1 | import "package:built_collection/built_collection.dart";
2 | import "package:built_value/built_value.dart";
3 |
4 | // ignore: prefer_double_quotes
5 | part 'user.g.dart';
6 |
7 | abstract class User implements Built {
8 | String get uid;
9 |
10 | String get email;
11 |
12 | String get name;
13 |
14 | @nullable
15 | String get status;
16 |
17 | // Keeps groupId : [channelId], marking the unread channels.
18 | @nullable
19 | BuiltMap get unreadUpdates;
20 |
21 | @nullable
22 | String get image;
23 |
24 | User._();
25 |
26 | factory User([void Function(UserBuilder) updates]) = _$User;
27 | }
28 |
29 | class UserHelper {
30 | static List userIds(List userIds) {
31 | if (userIds == null) return [];
32 | return userIds.whereType().toList();
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/lib/native_channels/android_permission_channel.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/native_channels/ios_permission_channel.dart";
2 | import "package:circles_app/util/logger.dart";
3 | import "package:flutter/foundation.dart";
4 | import "package:flutter/services.dart";
5 |
6 | const ANDROID_PERMISSION_GRANTED = 0;
7 |
8 | class AndroidPermissionChannel {
9 | static const MethodChannel channel =
10 | MethodChannel("de.janoodle.timy/permission-android");
11 |
12 | static Future requestPermission({
13 | @required PermissionType permissionType
14 | }) async {
15 | final result = await channel.invokeMethod(
16 | "requestPermission",
17 | {
18 | "permissionType": _stringOf(permissionType),
19 | },
20 | );
21 | final int granted = result[_stringOf(permissionType)];
22 | if (granted == ANDROID_PERMISSION_GRANTED) {
23 | Logger.d("Permission granted");
24 | return true;
25 | } else {
26 | Logger.w("Permission denied for ${_stringOf(permissionType)}");
27 | return false;
28 | }
29 | }
30 |
31 | static String _stringOf(PermissionType permissionType) {
32 | switch (permissionType) {
33 | case PermissionType.Photos:
34 | return "android.permission.READ_EXTERNAL_STORAGE";
35 | default:
36 | return "";
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/lib/native_channels/android_thumbnail_channel.dart:
--------------------------------------------------------------------------------
1 | import "dart:typed_data";
2 |
3 | import "package:flutter/material.dart";
4 | import "package:flutter/services.dart";
5 |
6 | const MethodChannel _channel = MethodChannel("de.janoodle.timy/thumbnails-android");
7 |
8 | Future getThumbnailBitmap({
9 | @required String fileId,
10 | @required int type,
11 | }) async {
12 | final Uint8List data = await _channel.invokeMethod(
13 | "getThumbnailBitmap",
14 | {
15 | "fileId": fileId,
16 | "type": type,
17 | },
18 | );
19 | return data;
20 | }
21 |
--------------------------------------------------------------------------------
/lib/native_channels/ios_permission_channel.dart:
--------------------------------------------------------------------------------
1 | import "package:flutter/foundation.dart";
2 | import "package:flutter/services.dart";
3 |
4 | enum PermissionType { Photos, Camera }
5 |
6 | class IOSPermissionChannel {
7 | static const MethodChannel channel =
8 | MethodChannel("de.janoodle.timy/permission-ios");
9 |
10 | static Future requestPermission({
11 | @required PermissionType permissionType
12 | }) {
13 | return channel.invokeMethod(
14 | "requestPermission",
15 | {
16 | "permissionType": _stringOf(permissionType),
17 | },
18 | );
19 | }
20 |
21 | static String _stringOf(PermissionType permissionType) {
22 | switch (permissionType) {
23 | case PermissionType.Camera:
24 | return "CAMERA";
25 | case PermissionType.Photos:
26 | return "PHOTOS";
27 | default:
28 | return "";
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/lib/native_channels/upload_platform.dart:
--------------------------------------------------------------------------------
1 | import "package:flutter/services.dart";
2 | import "package:meta/meta.dart";
3 |
4 | class UploadPlatform {
5 | static const MethodChannel channel =
6 | MethodChannel("de.janoodle.timy/upload_platform");
7 |
8 | Future uploadFiles({
9 | @required List filePaths, // Path to files on Android and a photo just taken on iOS
10 | @required List localIdentifiers, // iOS files localIdentifiers
11 | @required String groupId,
12 | @required String channelId,
13 | }) {
14 | return channel.invokeMethod(
15 | "uploadFiles",
16 | {
17 | "filePaths": filePaths,
18 | "localIdentifiers": localIdentifiers,
19 | "groupId": groupId,
20 | "channelId": channelId,
21 | },
22 | );
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/lib/presentation/calendar/calendar_item.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/model/channel.dart";
2 |
3 | abstract class CalendarItem {}
4 |
5 | class CalendarHeaderItem implements CalendarItem {
6 | final DateTime date;
7 | final bool isToday;
8 | final bool isPast;
9 | CalendarHeaderItem({
10 | this.date,
11 | this.isToday,
12 | this.isPast,
13 | });
14 | }
15 |
16 | class CalendarEntryItem implements CalendarItem {
17 | final String eventId;
18 | final String groupId;
19 | final String eventName;
20 | final String groupName;
21 | final DateTime date;
22 | final RSVP rsvpStatus;
23 | final bool isSelected;
24 | final bool isAllDay;
25 | final bool isPast;
26 | CalendarEntryItem({
27 | this.eventId,
28 | this.groupId,
29 | this.eventName,
30 | this.groupName,
31 | this.date,
32 | this.rsvpStatus,
33 | this.isSelected,
34 | this.isAllDay,
35 | this.isPast,
36 | });
37 | }
38 |
--------------------------------------------------------------------------------
/lib/presentation/channel/channel_screen_viewmodel.dart:
--------------------------------------------------------------------------------
1 | import "package:built_value/built_value.dart";
2 | import "package:circles_app/domain/redux/app_selector.dart";
3 | import "package:circles_app/domain/redux/app_state.dart";
4 | import "package:circles_app/model/channel.dart";
5 | import "package:circles_app/model/user.dart";
6 | import "package:redux/redux.dart";
7 |
8 | // ignore: prefer_double_quotes
9 | part 'channel_screen_viewmodel.g.dart';
10 |
11 | abstract class ChannelScreenViewModel
12 | implements Built {
13 | bool get isAuthor;
14 |
15 | bool get userIsMember;
16 |
17 | String get groupId;
18 |
19 | Channel get channel;
20 |
21 | User get user;
22 |
23 | bool get failedToJoin;
24 |
25 | RSVP get rsvpStatus;
26 |
27 | ChannelScreenViewModel._();
28 |
29 | factory ChannelScreenViewModel(
30 | [void Function(ChannelScreenViewModelBuilder) updates]) =
31 | _$ChannelScreenViewModel;
32 |
33 | static ChannelScreenViewModel fromStore(Store store) {
34 | final selectedChannel = getSelectedChannel(store.state);
35 | final hasSelectedChannel = selectedChannel != null;
36 | final channelUser = selectedChannel
37 | ?.users
38 | ?.firstWhere((u) => u.id == store.state.user.uid, orElse: () => null);
39 |
40 | return ChannelScreenViewModel((v) => v
41 | ..isAuthor =
42 | selectedChannel.authorId == store.state.user.uid
43 | ..userIsMember = hasSelectedChannel && channelUser != null
44 | ..groupId = hasSelectedChannel ? store.state.selectedGroupId : ""
45 | ..channel = selectedChannel.toBuilder()
46 | ..user = store.state.user.toBuilder()
47 | ..failedToJoin = store.state.channelState.joinChannelFailed
48 | ..rsvpStatus = channelUser?.rsvp ?? RSVP.UNSET);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/lib/presentation/channel/details/topic_details_viewmodel.dart:
--------------------------------------------------------------------------------
1 | import "package:built_collection/built_collection.dart";
2 | import "package:built_value/built_value.dart";
3 | import "package:circles_app/domain/redux/app_selector.dart";
4 | import "package:circles_app/domain/redux/app_state.dart";
5 | import "package:circles_app/model/channel.dart";
6 | import "package:circles_app/model/user.dart";
7 | import "package:redux/redux.dart";
8 |
9 | // ignore: prefer_double_quotes
10 | part 'topic_details_viewmodel.g.dart';
11 |
12 | abstract class TopicDetailsViewModel
13 | implements Built {
14 | String get name;
15 |
16 | String get description;
17 |
18 | ChannelVisibility get visibility;
19 |
20 | BuiltList get members;
21 |
22 | String get groupId;
23 |
24 | Channel get channel;
25 |
26 | String get userId;
27 |
28 | TopicDetailsViewModel._();
29 |
30 | factory TopicDetailsViewModel(
31 | [void Function(TopicDetailsViewModelBuilder) updates]) =
32 | _$TopicDetailsViewModel;
33 |
34 | static TopicDetailsViewModel fromStore(Store store) {
35 | final channel = getSelectedChannel(store.state);
36 | final members = store.state.groupUsers
37 | .where((user) => channel.users.any((u) => u.id == user.uid));
38 |
39 | return TopicDetailsViewModel((t) => t
40 | ..name = channel.name
41 | ..visibility = channel.visibility
42 | ..description = channel.description ?? ""
43 | ..members.addAll(members)
44 | ..groupId = store.state.selectedGroupId
45 | ..channel = channel.toBuilder()
46 | ..userId = store.state.user.uid);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/lib/presentation/channel/event/rsvp_dialog.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/circles_localization.dart";
2 | import "package:circles_app/model/channel.dart";
3 | import "package:circles_app/theme.dart";
4 | import "package:flutter/material.dart";
5 |
6 | showDialogRsvp(context, RSVP rsvp) {
7 | showDialog(
8 | context: context,
9 | builder: (context) {
10 | return Center(
11 | child: Card(
12 | child: Container(
13 | width: _Style.dialogWidth,
14 | height: _Style.dialogHeight,
15 | child: _dialogContent(context, rsvp),
16 | ),
17 | ),
18 | );
19 | });
20 | }
21 |
22 | String _rsvpIcon(RSVP rsvp) {
23 | switch (rsvp) {
24 | case RSVP.YES:
25 | return "assets/graphics/channel/rsvp/rsvp_yes_large.png";
26 | break;
27 | case RSVP.MAYBE:
28 | return "assets/graphics/channel/rsvp/rsvp_maybe_large.png";
29 | break;
30 | case RSVP.NO:
31 | return "assets/graphics/channel/rsvp/rsvp_no_large.png";
32 | break;
33 | case RSVP.UNSET:
34 | default:
35 | return "";
36 | break;
37 | }
38 | }
39 |
40 | Column _dialogContent(context, RSVP rsvp) {
41 | return Column(
42 | mainAxisAlignment: MainAxisAlignment.spaceEvenly,
43 | children: [
44 | Image.asset(
45 | _rsvpIcon(rsvp),
46 | height: _Style.dialogIconSize,
47 | width: _Style.dialogIconSize,
48 | ),
49 | _rsvpText(context, rsvp),
50 | ],
51 | );
52 | }
53 |
54 | Widget _rsvpText(context, RSVP rsvp) {
55 | switch (rsvp) {
56 | case RSVP.YES:
57 | return Text(
58 | CirclesLocalizations.of(context).eventRsvpDialogYes,
59 | style: AppTheme.dialogRsvpYesTextStyle,
60 | );
61 | case RSVP.MAYBE:
62 | return Text(
63 | CirclesLocalizations.of(context).eventRsvpDialogMaybe,
64 | style: AppTheme.dialogRsvpMaybeTextStyle,
65 | );
66 | case RSVP.NO:
67 | return Text(
68 | CirclesLocalizations.of(context).eventRsvpDialogNo,
69 | style: AppTheme.dialogRsvpNoTextStyle,
70 | );
71 | case RSVP.UNSET:
72 | default:
73 | return Container();
74 | }
75 | }
76 |
77 | class _Style {
78 | static const dialogWidth = 188.0;
79 | static const dialogHeight = 188.0;
80 | static const dialogIconSize = 64.0;
81 | }
82 |
--------------------------------------------------------------------------------
/lib/presentation/channel/input/chat_input_viewmodel.dart:
--------------------------------------------------------------------------------
1 | import "package:built_value/built_value.dart";
2 | import "package:circles_app/domain/redux/app_state.dart";
3 | import "package:circles_app/domain/redux/ui/ui_state_selector.dart";
4 | import "package:redux/redux.dart";
5 |
6 | // ignore: prefer_double_quotes
7 | part 'chat_input_viewmodel.g.dart';
8 |
9 | abstract class ChatInputViewModel
10 | implements Built {
11 |
12 | @nullable
13 | String get inputDraft;
14 |
15 | ChatInputViewModel._();
16 |
17 | factory ChatInputViewModel(
18 | [void Function(ChatInputViewModelBuilder) updates]) =
19 | _$ChatInputViewModel;
20 |
21 | static ChatInputViewModel fromStore(Store store) {
22 | return ChatInputViewModel((v) => v
23 | ..inputDraft = getInputDraftSelectedChannel(store.state));
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/lib/presentation/channel/input/send_button.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/circles_localization.dart";
2 | import "package:circles_app/domain/redux/app_state.dart";
3 | import "package:circles_app/domain/redux/message/message_actions.dart";
4 | import "package:circles_app/presentation/channel/messages_scroll_controller.dart";
5 | import "package:circles_app/theme.dart";
6 | import "package:flutter/material.dart";
7 | import "package:flutter_redux/flutter_redux.dart";
8 |
9 | class SendButton extends StatelessWidget {
10 | const SendButton({
11 | Key key,
12 | @required TextEditingController controller,
13 | @required bool enabled,
14 | }) : _controller = controller,
15 | _enabled = enabled,
16 | super(key: key);
17 |
18 | final TextEditingController _controller;
19 | final bool _enabled;
20 |
21 | @override
22 | Widget build(BuildContext context) {
23 | return FlatButton(
24 | child: Text(
25 | CirclesLocalizations.of(context).channelInputSend,
26 | style: AppTheme.buttonTextStyle,
27 | ),
28 | padding: EdgeInsets.all(16),
29 | disabledTextColor: AppTheme.colorTextDisabled,
30 | textColor: AppTheme.colorTextEnabled,
31 | onPressed: !_enabled
32 | ? null
33 | : () {
34 | final text = _controller.text;
35 | _controller.clear();
36 | StoreProvider.of(context).dispatch(
37 | SendMessage(text),
38 | );
39 | MessagesScrollController.of(context).scrollController.animateTo(
40 | 0.0,
41 | duration: Duration(milliseconds: 600),
42 | curve: Curves.fastOutSlowIn,
43 | );
44 | },
45 | );
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/lib/presentation/channel/invite/invite_to_channel_viewmodel.dart:
--------------------------------------------------------------------------------
1 | import "dart:async";
2 |
3 | import "package:built_collection/built_collection.dart";
4 | import "package:built_value/built_value.dart";
5 | import "package:circles_app/domain/redux/app_state.dart";
6 | import "package:circles_app/domain/redux/channel/channel_actions.dart";
7 | import "package:circles_app/model/user.dart";
8 | import "package:redux/redux.dart";
9 |
10 | // ignore: prefer_double_quotes
11 | part 'invite_to_channel_viewmodel.g.dart';
12 |
13 | abstract class InviteToChannelViewModel
14 | implements
15 | Built {
16 | InviteToChannelViewModel._();
17 |
18 | BuiltList get newUsers;
19 |
20 | @BuiltValueField(compare: false)
21 | void Function(Iterable, Completer) get inviteToChannel;
22 |
23 | factory InviteToChannelViewModel(
24 | [void Function(InviteToChannelViewModelBuilder) updates]) =
25 | _$InviteToChannelViewModel;
26 |
27 | static InviteToChannelViewModel Function(Store store) fromStore(
28 | String channelId) {
29 | return (Store store) {
30 | final selectedGroup = store.state.selectedGroupId;
31 | final channel = store.state.groups[selectedGroup].channels[channelId];
32 |
33 | // Filter out any user that is already part of the channel
34 | final newUsers = store.state.groupUsers
35 | .where((user) => !channel.users.any((u) => u.id == user.uid));
36 |
37 | return InviteToChannelViewModel((vm) => vm
38 | ..newUsers.replace(newUsers)
39 | ..inviteToChannel = (users, completer) =>
40 | store.dispatch(InviteToChannelAction(users, channel, completer)));
41 | };
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/lib/presentation/channel/join_channel.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/circles_localization.dart";
2 | import "package:circles_app/domain/redux/app_state.dart";
3 | import "package:circles_app/domain/redux/channel/channel_actions.dart";
4 | import "package:circles_app/model/channel.dart";
5 | import "package:circles_app/model/user.dart";
6 | import "package:flutter/cupertino.dart";
7 | import "package:flutter/material.dart";
8 | import "package:flutter_redux/flutter_redux.dart";
9 |
10 | class JoinChannel extends StatelessWidget {
11 | final String _groupId;
12 | final Channel _channel;
13 | final User _user;
14 |
15 | const JoinChannel(this._groupId, this._channel, this._user);
16 |
17 | @override
18 | Widget build(BuildContext context) {
19 | return Padding(
20 | padding: const EdgeInsets.only(top: 30.0, bottom: 40.0),
21 | child: Container(
22 | child: Row(
23 | crossAxisAlignment: CrossAxisAlignment.start,
24 | children: [
25 | Expanded(
26 | child: Column(
27 | children: [
28 | Container(
29 | child: Text(
30 | CirclesLocalizations.of(context).channelJoinMessage),
31 | ),
32 | Container(
33 | child: RaisedButton(
34 | color: Colors.blue,
35 | textColor: Colors.white,
36 | child: Text(CirclesLocalizations.of(context).channelJoin),
37 | onPressed: () {
38 | StoreProvider.of(context)
39 | .dispatch(JoinChannelAction(groupId: _groupId, channel: _channel, user: _user));
40 | },
41 | ))
42 | ],
43 | )),
44 | ],
45 | ),
46 | ));
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/lib/presentation/channel/message/message_body.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/model/message.dart";
2 | import "package:circles_app/theme.dart";
3 | import "package:flutter/cupertino.dart";
4 | import "package:flutter/material.dart";
5 | import "package:flutter/widgets.dart";
6 | import "package:flutter_linkify/flutter_linkify.dart";
7 | import "package:linkify/linkify.dart";
8 | import "package:url_launcher/url_launcher.dart";
9 |
10 | class MessageBody extends StatelessWidget {
11 | const MessageBody({
12 | Key key,
13 | @required Message message,
14 | }) : _message = message,
15 | super(key: key);
16 |
17 | final Message _message;
18 |
19 | @override
20 | Widget build(BuildContext context) {
21 | if (_message.body.isEmpty) {
22 | return SizedBox.shrink();
23 | }
24 | final elements = linkify(
25 | _message.body,
26 | humanize: true,
27 | linkTypes: null,
28 | );
29 | return RichText(
30 | textScaleFactor: MediaQuery.of(context).textScaleFactor,
31 | text: buildTextSpan(
32 | elements,
33 | style: AppTheme.messageTextStyle,
34 | onOpen: (link) async {
35 | if (await canLaunch(link.url)) {
36 | await launch(link.url);
37 | } else {
38 | throw "Could not launch $link";
39 | }
40 | },
41 | linkStyle: AppTheme.linkTextStyle,
42 | ),
43 | );
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/lib/presentation/channel/message/message_timestamp.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/model/message.dart";
2 | import "package:circles_app/model/user.dart";
3 | import "package:circles_app/theme.dart";
4 | import "package:flutter/material.dart";
5 | import "package:intl/intl.dart";
6 |
7 | class MessageTimestamp extends StatelessWidget {
8 | const MessageTimestamp({
9 | Key key,
10 | @required Message message,
11 | @required User currentUser,
12 | }) : _message = message,
13 | _currentUser = currentUser,
14 | super(key: key);
15 |
16 | final Message _message;
17 | final User _currentUser;
18 |
19 | @override
20 | Widget build(BuildContext context) {
21 | final timestamp = Text(
22 | DateFormat.Hm().format(_message.timestamp),
23 | style: AppTheme.messageTimestampTextStyle,
24 | );
25 | return Padding(
26 | padding: const EdgeInsets.only(left: 8.0),
27 | child: AnimatedSwitcher(
28 | duration: Duration(milliseconds: 500),
29 | child: AnimatedSwitcher(
30 | child: _message.authorId == _currentUser.uid && _message.pending
31 | ? _buildLoading()
32 | : timestamp,
33 | duration: Duration(milliseconds: 200),
34 | ),
35 | ),
36 | );
37 | }
38 |
39 | Widget _buildLoading() {
40 | return Icon(
41 | Icons.cached,
42 | size: 16.0,
43 | color: Colors.grey,
44 | );
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/lib/presentation/channel/message/system_message_item.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/circles_localization.dart";
2 | import "package:circles_app/model/message.dart";
3 | import "package:circles_app/theme.dart";
4 | import "package:flutter/material.dart";
5 |
6 | class SystemMessageItem extends StatelessWidget {
7 | final Message _message;
8 |
9 | const SystemMessageItem(
10 | this._message, {
11 | Key key,
12 | }) : super(key: key);
13 |
14 | @override
15 | Widget build(BuildContext context) {
16 | return Container(
17 | height: 48 * AppTheme.pixelMultiplier,
18 | width: MediaQuery.of(context).size.width,
19 | child: Center(
20 | child: Padding(
21 | padding: const EdgeInsets.only(
22 | left: AppTheme.appMargin,
23 | right: AppTheme.appMargin,
24 | ),
25 | // Currently only dealing with SYSTEM or RSVP messages
26 | child: Text(_message.messageType == MessageType.SYSTEM ?
27 | CirclesLocalizations.of(context).channelSystemMessage(_message.body).toUpperCase() :
28 | CirclesLocalizations.of(context).rsvpSystemMessage(_message.body).toUpperCase(),
29 | style: AppTheme.systemMessageTextStyle,
30 | textAlign: TextAlign.center,
31 | ),
32 | ),
33 | ),
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/lib/presentation/channel/messages_list/messages_list.dart:
--------------------------------------------------------------------------------
1 | import "dart:io";
2 |
3 | import "package:circles_app/domain/redux/app_state.dart";
4 | import "package:circles_app/model/message.dart";
5 | import "package:circles_app/presentation/channel/message/message_item.dart";
6 | import "package:circles_app/presentation/channel/message/system_message_item.dart";
7 | import "package:circles_app/presentation/channel/messages_list/messages_list_viewmodel.dart";
8 | import "package:flutter/material.dart";
9 | import "package:flutter_redux/flutter_redux.dart";
10 |
11 | class MessagesList extends StatelessWidget {
12 | const MessagesList({
13 | Key key,
14 | @required this.scrollController,
15 | }) : super(key: key);
16 |
17 | final ScrollController scrollController;
18 |
19 | @override
20 | Widget build(BuildContext context) {
21 | return GestureDetector(
22 | onTapUp: (details) {
23 | // On iOS, taping on the chat section dismisses keyboard
24 | if (Platform.isIOS) {
25 | FocusScope.of(context).requestFocus(FocusNode());
26 | }
27 | },
28 | child: StoreConnector(
29 | builder: (context, vm) {
30 | return ListView.builder(
31 | controller: scrollController,
32 | reverse: true,
33 | itemCount: vm.messages.length,
34 | itemBuilder: (context, index) {
35 | final message = vm.messages[index];
36 | return _selectMessageBuilder(message, vm);
37 | });
38 | },
39 | converter: MessagesListViewModel.fromStore,
40 | distinct: true,
41 | ),
42 | );
43 | }
44 |
45 | Widget _selectMessageBuilder(
46 | Message message,
47 | MessagesListViewModel vm,
48 | ) {
49 | switch (message.messageType) {
50 | case MessageType.SYSTEM:
51 | case MessageType.RSVP:
52 | return SystemMessageItem(message);
53 | break;
54 | case MessageType.USER:
55 | case MessageType.MEDIA:
56 | return MessageItem(
57 | message: message,
58 | currentUser: vm.currentUser,
59 | userIsMember: vm.userIsMember,
60 | author: vm.authors[message.authorId],
61 | );
62 | break;
63 | default:
64 | return SizedBox.shrink();
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/lib/presentation/channel/messages_list/messages_list_viewmodel.dart:
--------------------------------------------------------------------------------
1 | import "package:built_collection/built_collection.dart";
2 | import "package:built_value/built_value.dart";
3 | import "package:circles_app/domain/redux/app_selector.dart";
4 | import "package:circles_app/domain/redux/app_state.dart";
5 | import "package:circles_app/model/message.dart";
6 | import "package:circles_app/model/user.dart";
7 | import "package:redux/redux.dart";
8 |
9 | // ignore: prefer_double_quotes
10 | part 'messages_list_viewmodel.g.dart';
11 |
12 | abstract class MessagesListViewModel
13 | implements Built {
14 | @nullable
15 | User get currentUser;
16 |
17 | BuiltList get messages;
18 |
19 | bool get userIsMember;
20 |
21 | BuiltMap get authors;
22 |
23 | MessagesListViewModel._();
24 |
25 | factory MessagesListViewModel(
26 | [void Function(MessagesListViewModelBuilder) updates]) =
27 | _$MessagesListViewModel;
28 |
29 | static MessagesListViewModel fromStore(Store store) {
30 | return MessagesListViewModel((m) => m
31 | ..messages = store.state.messagesOnScreen.toBuilder()
32 | ..currentUser = store.state.user?.toBuilder()
33 | ..authors = MapBuilder(
34 | store.state.groupUsers.asMap().map((k, v) => MapEntry(v.uid, v)))
35 | ..userIsMember = getSelectedChannel(store.state)?.users
36 | ?.any((u) => u.id == store.state.user.uid));
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/lib/presentation/channel/messages_scroll_controller.dart:
--------------------------------------------------------------------------------
1 | import "package:flutter/material.dart";
2 |
3 | class MessagesScrollController extends InheritedWidget {
4 | final ScrollController scrollController;
5 |
6 | const MessagesScrollController({
7 | Key key,
8 | @required Widget child,
9 | @required this.scrollController,
10 | }) : super(key: key, child: child);
11 |
12 | @override
13 | bool updateShouldNotify(InheritedWidget oldWidget) {
14 | return true;
15 | }
16 |
17 | static MessagesScrollController of(BuildContext context) =>
18 | context.inheritFromWidgetOfExactType(MessagesScrollController);
19 | }
20 |
--------------------------------------------------------------------------------
/lib/presentation/channel/reaction/emoji_picker.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/domain/redux/app_state.dart";
2 | import "package:circles_app/domain/redux/message/message_actions.dart";
3 | import "package:circles_app/model/message.dart";
4 | import "package:circles_app/theme.dart";
5 | import "package:flutter/cupertino.dart";
6 | import "package:flutter/material.dart";
7 | import "package:flutter_redux/flutter_redux.dart";
8 |
9 | final emojiPickerOptions = const [
10 | "❤️",
11 | "😂",
12 | "🔥",
13 | "😍",
14 | "👍",
15 | "🤔",
16 | "👽",
17 | "😊",
18 | "🥰",
19 | ];
20 |
21 | void showEmojiPicker(BuildContext context, Message message) {
22 | showCupertinoModalPopup(
23 | context: context,
24 | builder: (context) {
25 | return EmojiPicker(message);
26 | });
27 | }
28 |
29 | class EmojiPicker extends StatelessWidget {
30 | final Message _message;
31 |
32 | const EmojiPicker(this._message);
33 |
34 | @override
35 | Widget build(BuildContext context) {
36 | return SafeArea(
37 | child: Padding(
38 | padding: const EdgeInsets.all(16.0),
39 | child: Container(
40 | decoration: BoxDecoration(
41 | borderRadius: BorderRadius.all(Radius.circular(16)),
42 | color: Colors.white,
43 | ),
44 | child: Padding(
45 | padding: const EdgeInsets.all(8.0),
46 | child: Wrap(
47 | children: emojiPickerOptions.map((emoji) =>
48 | Material(
49 | color: Colors.white,
50 | child: InkWell(
51 | child: Padding(
52 | padding: const EdgeInsets.all(8.0),
53 | child: Text(
54 | emoji,
55 | style: TextStyle(fontSize: 20 * AppTheme.pixelMultiplier),
56 | ),
57 | ),
58 | onTap: () {
59 | StoreProvider.of(context).dispatch(EmojiReaction(
60 | _message.id,
61 | emoji,
62 | ));
63 | Navigator.of(context).pop();
64 | },
65 | ),
66 | )
67 | ).toList()
68 | ),
69 | ),
70 | ),
71 | ),
72 | );
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/lib/presentation/channel/reaction/reaction_button.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/model/message.dart";
2 | import "package:circles_app/presentation/channel/reaction/emoji_picker.dart";
3 | import "package:circles_app/presentation/channel/reaction/reaction.dart";
4 | import "package:circles_app/theme.dart";
5 | import "package:flutter/cupertino.dart";
6 | import "package:flutter/material.dart";
7 |
8 | class ReactionButton extends StatelessWidget {
9 | const ReactionButton(
10 | this._message,
11 | );
12 |
13 | final Message _message;
14 |
15 | @override
16 | Widget build(BuildContext context) {
17 | return InkWell(
18 | child: emojiBorder(
19 | Image.asset(
20 | "assets/graphics/icon_smile.png",
21 | height: 16 * AppTheme.pixelMultiplier,
22 | width: 16 * AppTheme.pixelMultiplier,
23 | ),
24 | ),
25 | onTap: () {
26 | showEmojiPicker(context, _message);
27 | },
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/lib/presentation/channel/reaction/reaction_detail_data.dart:
--------------------------------------------------------------------------------
1 | import "package:built_value/built_value.dart";
2 |
3 | // ignore: prefer_double_quotes
4 | part 'reaction_detail_data.g.dart';
5 |
6 | abstract class ReactionDetailData
7 | implements Built {
8 | String get emoji;
9 |
10 | String get names;
11 |
12 | ReactionDetailData._();
13 |
14 | factory ReactionDetailData(
15 | [void Function(ReactionDetailDataBuilder) updates]) =
16 | _$ReactionDetailData;
17 | }
18 |
--------------------------------------------------------------------------------
/lib/presentation/channel/reaction/reaction_section.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/model/message.dart";
2 | import "package:circles_app/model/user.dart";
3 | import "package:circles_app/presentation/channel/reaction/reaction.dart";
4 | import "package:circles_app/presentation/channel/reaction/reaction_button.dart";
5 | import "package:circles_app/routes.dart";
6 | import "package:flutter/material.dart";
7 |
8 | class ReactionSection extends StatelessWidget {
9 | const ReactionSection({
10 | Key key,
11 | @required Message message,
12 | @required User currentUser,
13 | @required bool userIsMember,
14 | }) : _message = message,
15 | _currentUser = currentUser,
16 | _userIsMember = userIsMember,
17 | super(key: key);
18 |
19 | final Message _message;
20 | final User _currentUser;
21 | final bool _userIsMember;
22 |
23 | @override
24 | Widget build(BuildContext context) {
25 | final userEmoji = _message.reactions[_currentUser.uid];
26 |
27 | final list = [];
28 |
29 | _message.reactionsCount().forEach((emoji, count) {
30 | final isUserEmoji = userEmoji?.emoji == emoji;
31 | list.add(Reaction(
32 | emoji: emoji,
33 | count: count,
34 | isUserEmoji: isUserEmoji,
35 | messageId: _message.id,
36 | ));
37 | });
38 |
39 | if (list.isNotEmpty &&
40 | _currentUser.uid != _message.authorId &&
41 | !_message.reactions.containsKey(_currentUser.uid) &&
42 | _userIsMember) {
43 | list.add(ReactionButton(_message));
44 | }
45 |
46 | return Padding(
47 | padding: const EdgeInsets.only(top: 8.0),
48 | child: InkWell(
49 | onLongPress: () {
50 | Navigator.of(context).pushNamed(
51 | Routes.reaction,
52 | arguments: _message.reactions,
53 | );
54 | },
55 | // Wrap takes care of showing the each reaction one after the other
56 | // and when it runs out of space, will go to the next line.
57 | child: Wrap(
58 | spacing: 8.0,
59 | runSpacing: 8.0,
60 | direction: Axis.horizontal,
61 | children: list,
62 | ),
63 | ),
64 | );
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/lib/presentation/common/color_label_text_form_field.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/theme.dart";
2 | import "package:flutter/material.dart";
3 |
4 | ///
5 | /// This is a TextFormField which will change the Label color
6 | /// when it is not empty.
7 | ///
8 | /// i.e. When the content is not empty, the whole TextFormField
9 | /// becomes the normal color: the border and the label.
10 | ///
11 | /// If you are also using validation, use ErrorLabelTextFormField
12 | ///
13 | class ColorLabelTextFormField extends StatefulWidget {
14 | const ColorLabelTextFormField({
15 | Key key,
16 | String labelText,
17 | String helperText,
18 | @required TextEditingController controller,
19 | }) : _controller = controller,
20 | _labelText = labelText,
21 | _helperText = helperText,
22 | super(key: key);
23 |
24 | final TextEditingController _controller;
25 | final String _labelText;
26 | final String _helperText;
27 |
28 | @override
29 | _ColorLabelTextFormFieldState createState() =>
30 | _ColorLabelTextFormFieldState();
31 | }
32 |
33 | class _ColorLabelTextFormFieldState extends State {
34 | bool _isEmpty = true;
35 |
36 | @override
37 | void initState() {
38 | super.initState();
39 | widget._controller.addListener(() {
40 | setState(() {
41 | _isEmpty = widget._controller.text.isEmpty;
42 | });
43 | });
44 | }
45 |
46 | @override
47 | Widget build(BuildContext context) {
48 | final theme = _isEmpty
49 | ? AppTheme.inputDecorationEmptyTheme
50 | : AppTheme.inputDecorationFilledTheme;
51 |
52 | return TextFormField(
53 | style: AppTheme.inputMediumTextStyle,
54 | decoration: InputDecoration(
55 | labelText: widget._labelText,
56 | helperText: widget._helperText,
57 | ).applyDefaults(theme),
58 | controller: widget._controller,
59 | );
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/lib/presentation/common/modal_item.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/theme.dart";
2 | import "package:flutter/cupertino.dart";
3 |
4 | class ModalItem extends StatelessWidget {
5 | const ModalItem({
6 | Key key,
7 | this.iconAsset,
8 | this.iconData,
9 | @required this.label,
10 | }) : super(key: key);
11 |
12 | final String iconAsset;
13 | final String label;
14 | final IconData iconData;
15 |
16 | @override
17 | Widget build(BuildContext context) {
18 | return Row(
19 | children: [
20 | Padding(
21 | padding: const EdgeInsets.only(
22 | left: 8.0,
23 | right: 8.0,
24 | ),
25 | child: iconAsset != null
26 | ? Image.asset(
27 | iconAsset,
28 | scale: 3,
29 | )
30 | : Icon(iconData),
31 | ),
32 | Text(
33 | label,
34 | style: AppTheme.optionTextStyle,
35 | textScaleFactor: 1,
36 | ),
37 | ],
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/lib/presentation/common/platform_alerts.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/circles_localization.dart";
2 | import "package:flutter/widgets.dart";
3 | import "package:flutter_platform_widgets/flutter_platform_widgets.dart";
4 |
5 | enum AccessResourceType { CAMERA, STORAGE }
6 |
7 | showNoAccessAlert({
8 | AccessResourceType type,
9 | BuildContext context,
10 | }) {
11 | final dialog = PlatformAlertDialog(
12 | title: Text(CirclesLocalizations.of(context).platformAlertAccessTitle),
13 | content:
14 | Text(CirclesLocalizations.of(context).platformAlertAccessBody(type)),
15 | actions: [
16 | PlatformDialogAction(
17 | child: PlatformText(CirclesLocalizations.of(context).ok),
18 | onPressed: () {
19 | Navigator.pop(context);
20 | }),
21 | ],
22 | );
23 |
24 | return showPlatformDialog(
25 | context: context,
26 | builder: (_) => dialog,
27 | );
28 | }
29 |
30 | /// Present PlatformDialog for fetures yet to implement.
31 | showSoonAlert({BuildContext context}) {
32 | final actions = [
33 | PlatformDialogAction(
34 | child: PlatformText(CirclesLocalizations.of(context).cancel),
35 | onPressed: () {
36 | Navigator.pop(context);
37 | },
38 | ),
39 | ];
40 |
41 | return showPlatformDialog(
42 | context: context,
43 | builder: (_) => PlatformAlertDialog(
44 | title: Text(CirclesLocalizations.of(context).genericSoonAlertTitle),
45 | content: Text(CirclesLocalizations.of(context).genericSoonAlertMessage),
46 | actions: actions,
47 | ),
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/lib/presentation/common/round_button.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/theme.dart";
2 | import "package:flutter/material.dart";
3 | import "package:flutter/rendering.dart";
4 |
5 | class RoundButton extends StatelessWidget {
6 | const RoundButton({
7 | @required this.text,
8 | @required this.onTap,
9 | });
10 |
11 | final String text;
12 | final Function onTap;
13 |
14 | @override
15 | Widget build(BuildContext context) {
16 | final borderRadius = BorderRadius.circular(24);
17 | return Material(
18 | color: Colors.transparent,
19 | borderRadius: borderRadius,
20 | child: InkWell(
21 | onTap: onTap,
22 | borderRadius: borderRadius,
23 | child: Container(
24 | decoration: BoxDecoration(
25 | borderRadius: borderRadius,
26 | border: Border.all(
27 | color: AppTheme.colorDarkBlue
28 | )
29 | ),
30 | height: 40,
31 | width: 200,
32 | child: Center(
33 | child: Text(
34 | text,
35 | style: AppTheme.buttonTextStyle.apply(
36 | color: AppTheme.colorDarkBlue
37 | ),
38 | ),
39 | ),
40 | ),
41 | ),
42 | );
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/lib/presentation/home/channel_list/group_status_icon_widget.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/theme.dart";
2 | import "package:flutter/widgets.dart";
3 |
4 | class GroupStatusIconWidget extends StatelessWidget {
5 | final bool _joined;
6 | final bool _isPrivateChannel;
7 |
8 | const GroupStatusIconWidget({joined, isPrivateChannel, Key key})
9 | : _joined = joined,
10 | _isPrivateChannel = isPrivateChannel,
11 | super(key: key);
12 |
13 | @override
14 | Widget build(BuildContext context) {
15 | return Container(
16 | child: Container(
17 | width: _Style.width,
18 | height: _Style.width,
19 | child: Padding(
20 | padding: EdgeInsets.only(top: 5.0),
21 | child: Stack(
22 | children: [
23 | Image.asset(
24 | _joined
25 | ? "assets/graphics/channel/topic_joined.png"
26 | : "assets/graphics/channel/topic_open.png",
27 | width: _Style.imageSize.width,
28 | color: AppTheme.colorDarkBlue,
29 | ),
30 | Visibility(
31 | visible: _isPrivateChannel,
32 | child: Image.asset(
33 | "assets/graphics/channel/padlock.png",
34 | width: _Style.imageSize.width,
35 | height: _Style.imageSize.height,
36 | ),
37 | ),
38 | ],
39 | ))));
40 | }
41 | }
42 |
43 | class _Style {
44 | static const imageSize = Size(25, 26);
45 | static const width = 32.0;
46 | }
47 |
--------------------------------------------------------------------------------
/lib/presentation/home/circles_drawer.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/presentation/calendar/calendar_screen.dart";
2 | import "package:circles_app/presentation/home/channel_list/channel_list.dart";
3 | import "package:circles_app/presentation/home/group_list/group_list.dart";
4 | import "package:circles_app/theme.dart";
5 | import "package:flutter/material.dart";
6 |
7 | enum DrawerState { CALENDAR, CHANNEL }
8 |
9 | class CirclesDrawer extends StatefulWidget {
10 | @override
11 | _CirclesDrawerState createState() => _CirclesDrawerState();
12 | }
13 |
14 | class _CirclesDrawerState extends State {
15 | DrawerState _drawerState = DrawerState.CHANNEL;
16 |
17 | _drawerStateChange(DrawerState state) {
18 | setState(() {
19 | _drawerState = state;
20 | });
21 | }
22 |
23 | @override
24 | Widget build(BuildContext context) {
25 | return Drawer(
26 | child: Container(
27 | decoration: BoxDecoration(
28 | color: AppTheme.colorMintGreen,
29 | image: DecorationImage(
30 | colorFilter: ColorFilter.mode(
31 | Color.fromRGBO(255, 255, 255, 0.1),
32 | BlendMode.modulate,
33 | ),
34 | image: AssetImage("assets/graphics/visual_twist_white_petrol.png"),
35 | fit: BoxFit.cover,
36 | ),
37 | ),
38 | child: Row(
39 | crossAxisAlignment: CrossAxisAlignment.start,
40 | children: [
41 | GroupList(_drawerStateChange),
42 | _drawerState == DrawerState.CALENDAR
43 | ? CalendarScreen()
44 | : ChannelsList()
45 | ],
46 | ),
47 | ));
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/lib/presentation/home/group_list/group_list_viewmodel.dart:
--------------------------------------------------------------------------------
1 | import "package:built_collection/built_collection.dart";
2 | import "package:built_value/built_value.dart";
3 | import "package:circles_app/domain/redux/app_state.dart";
4 | import "package:circles_app/model/group.dart";
5 | import "package:redux/redux.dart";
6 |
7 | // ignore: prefer_double_quotes
8 | part 'group_list_viewmodel.g.dart';
9 |
10 | abstract class GroupListViewModel
11 | implements Built {
12 | BuiltList get groups;
13 | BuiltList get updatedGroups;
14 | String get selectedGroupId;
15 |
16 | GroupListViewModel._();
17 |
18 | factory GroupListViewModel(
19 | [void Function(GroupListViewModelBuilder) updates]) =
20 | _$GroupListViewModel;
21 |
22 | static GroupListViewModel fromStore(Store store) {
23 | final unreadGroupsMap = store.state.user.unreadUpdates.toMap();
24 | unreadGroupsMap.removeWhere((key, value) => value == null || value.length == 0);
25 |
26 | return GroupListViewModel((c) => c
27 | ..groups = ListBuilder(store.state.groups.values)
28 | ..selectedGroupId = store.state.selectedGroupId
29 | ..updatedGroups = ListBuilder(unreadGroupsMap.keys));
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/lib/presentation/home/home_app_bar_viewmodel.dart:
--------------------------------------------------------------------------------
1 | import "package:built_value/built_value.dart";
2 | import "package:circles_app/circles_localization.dart";
3 | import "package:circles_app/domain/redux/app_selector.dart";
4 | import "package:circles_app/domain/redux/app_state.dart";
5 | import "package:circles_app/model/channel.dart";
6 | import "package:circles_app/util/date_formatting.dart";
7 | import "package:redux/redux.dart";
8 |
9 | // ignore: prefer_double_quotes
10 | part 'home_app_bar_viewmodel.g.dart';
11 |
12 | abstract class HomeAppBarViewModel
13 | implements Built {
14 | HomeAppBarViewModel._();
15 |
16 | factory HomeAppBarViewModel(
17 | [void Function(HomeAppBarViewModelBuilder) updates]) =
18 | _$HomeAppBarViewModel;
19 |
20 | bool get hasUpdatedChannelsInGroup;
21 |
22 | String get title;
23 |
24 | bool get memberOfChannel;
25 |
26 | bool get isEvent;
27 |
28 | String get eventDate;
29 |
30 | static Function(Store) fromStore(context) {
31 | return (Store store) {
32 | final channel = getSelectedChannel(store.state);
33 | final groupId = store.state.selectedGroupId;
34 | final channels = store.state.groups[groupId].channels.values.toList();
35 | final hasGroupUpdates =
36 | channels.any((c) => (c != channel) && c.hasUpdates);
37 |
38 | final isMemberOfChannel =
39 | channel.users.any((u) => u.id == store.state.user.uid);
40 |
41 | return HomeAppBarViewModel((vm) {
42 | return vm
43 | ..title = channel.name
44 | ..memberOfChannel = isMemberOfChannel
45 | ..hasUpdatedChannelsInGroup = hasGroupUpdates
46 | ..isEvent = channel.type == ChannelType.EVENT
47 | ..eventDate = _formatDate(context, channel);
48 | });
49 | };
50 | }
51 |
52 | static String _formatDate(context, Channel channel) {
53 | if (channel.startDate == null) {
54 | return "";
55 | }
56 | try {
57 | if (channel.hasStartTime) {
58 | return "${formatDate(context, channel.startDate)} "
59 | "${CirclesLocalizations.of(context).at} "
60 | "${formatTime(context, channel.startDate)}";
61 | } else {
62 | return formatDate(context, channel.startDate);
63 | }
64 | } catch (error) {
65 | return "";
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/lib/presentation/home/homescreen.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/domain/redux/app_state.dart";
2 | import "package:circles_app/presentation/channel/channel_screen.dart";
3 | import "package:circles_app/presentation/home/circles_drawer.dart";
4 | import "package:circles_app/presentation/home/home_app_bar.dart";
5 | import "package:circles_app/presentation/home/in_app_notification/in_app_notification_viewmodel.dart";
6 | import "package:circles_app/presentation/home/in_app_notification/in_app_notification_widget.dart";
7 | import "package:flutter/material.dart";
8 | import "package:flutter_redux/flutter_redux.dart";
9 |
10 | class HomeScreen extends StatefulWidget {
11 | final ValueNotifier sideOpenController;
12 |
13 | const HomeScreen({
14 | Key key,
15 | @required this.sideOpenController,
16 | }) : super(key: key);
17 |
18 | @override
19 | _HomeScreenState createState() => _HomeScreenState();
20 | }
21 |
22 | class _HomeScreenState extends State {
23 | final _scaffoldKey = GlobalKey();
24 |
25 | @override
26 | Widget build(BuildContext context) {
27 | return Stack(
28 | children: [
29 | Scaffold(
30 | key: _scaffoldKey,
31 | appBar: HomeAppBar(
32 | scaffoldKey: _scaffoldKey,
33 | sideOpenController: widget.sideOpenController,
34 | ),
35 | body: ChannelScreen(),
36 | drawer: CirclesDrawer(),
37 | ),
38 | StoreConnector(
39 | builder: (BuildContext context, InAppNotificationViewModel vm) {
40 | return vm.inAppNotification != null
41 | ? InAppNotificationWidget(vm)
42 | : Container();
43 | },
44 | converter: InAppNotificationViewModel.fromStore,
45 | distinct: true,
46 | ),
47 | ],
48 | );
49 | }
50 | }
51 |
52 |
--------------------------------------------------------------------------------
/lib/presentation/home/in_app_notification/in_app_notification_viewmodel.dart:
--------------------------------------------------------------------------------
1 | import "package:built_value/built_value.dart";
2 | import "package:circles_app/domain/redux/app_actions.dart";
3 | import "package:circles_app/domain/redux/app_state.dart";
4 | import "package:circles_app/domain/redux/channel/channel_actions.dart";
5 | import "package:circles_app/domain/redux/push/push_actions.dart";
6 | import "package:circles_app/model/in_app_notification.dart";
7 | import "package:redux/redux.dart";
8 |
9 | // ignore: prefer_double_quotes
10 | part 'in_app_notification_viewmodel.g.dart';
11 |
12 | abstract class InAppNotificationViewModel
13 | implements
14 | Built {
15 | @nullable
16 | InAppNotification get inAppNotification;
17 |
18 | @BuiltValueField(compare: false)
19 | Function get onDismissed;
20 |
21 | @BuiltValueField(compare: false)
22 | Function get onTap;
23 |
24 | InAppNotificationViewModel._();
25 |
26 | factory InAppNotificationViewModel(
27 | [void Function(InAppNotificationViewModelBuilder) updates]) =
28 | _$InAppNotificationViewModel;
29 |
30 | static InAppNotificationViewModel fromStore(Store store) {
31 | final previouslySelectedChannel = store.state.channelState.selectedChannel;
32 | return InAppNotificationViewModel((i) => i
33 | ..inAppNotification = store.state.inAppNotification?.toBuilder()
34 | ..onDismissed = () {
35 | store.dispatch(OnPushNotificationDismissedAction());
36 | }
37 | ..onTap = () {
38 | store.dispatch(SelectGroup(store.state.inAppNotification.groupId));
39 | store.dispatch(SelectChannel(
40 | previousChannelId: previouslySelectedChannel,
41 | channel: store.state.inAppNotification.channel,
42 | groupId: store.state.inAppNotification.groupId,
43 | userId: store.state.user.uid,
44 | ));
45 | store.dispatch(OnPushNotificationDismissedAction());
46 | });
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/lib/presentation/home/main_screen.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/domain/redux/app_state.dart";
2 | import "package:circles_app/model/channel.dart";
3 | import "package:circles_app/presentation/channel/details/topic_details.dart";
4 | import "package:circles_app/presentation/channel/event/event_details.dart";
5 | import "package:circles_app/presentation/home/homescreen.dart";
6 | import "package:circles_app/presentation/home/main_screen_viewmodel.dart";
7 | import "package:circles_app/presentation/home/slide_out_screen.dart";
8 | import "package:flutter/material.dart";
9 | import "package:flutter_redux/flutter_redux.dart";
10 |
11 | ///
12 | /// This screen loads the HomeScreen when there's data loaded
13 | /// to avoid things like having null User, null Channel, etc.
14 | ///
15 | /// Also holds the ValueNotifier for the side open/closed state
16 | /// as it passes it to the SlideOut widget and the HomeScreen.
17 | ///
18 | class MainScreen extends StatefulWidget {
19 | @override
20 | _MainScreenState createState() => _MainScreenState();
21 | }
22 |
23 | class _MainScreenState extends State {
24 | ValueNotifier _sideOpenController;
25 |
26 | @override
27 | void initState() {
28 | super.initState();
29 | _sideOpenController = ValueNotifier(false);
30 | }
31 |
32 | @override
33 | void dispose() {
34 | super.dispose();
35 | _sideOpenController.dispose();
36 | }
37 |
38 | @override
39 | Widget build(BuildContext context) {
40 | return StoreConnector(
41 | distinct: true,
42 | converter: MainScreenViewModel.fromStore,
43 | builder: (context, vm) {
44 | if (vm.hasData) {
45 | return SlideOutScreen(
46 | main: HomeScreen(
47 | sideOpenController: _sideOpenController,
48 | ),
49 | side: _buildDetails(vm),
50 | sideOpenController: _sideOpenController,
51 | );
52 | } else {
53 | // TODO: Proper empty state screen
54 | return Scaffold();
55 | }
56 | },
57 | );
58 | }
59 |
60 | Widget _buildDetails(MainScreenViewModel vm) {
61 | switch (vm.channelType) {
62 | case ChannelType.TOPIC:
63 | return TopicDetails(
64 | sideOpenController: _sideOpenController,
65 | );
66 | case ChannelType.EVENT:
67 | return EventDetails(
68 | sideOpenController: _sideOpenController,
69 | );
70 | }
71 | return null;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/lib/presentation/home/main_screen_viewmodel.dart:
--------------------------------------------------------------------------------
1 | import "package:built_value/built_value.dart";
2 | import "package:circles_app/domain/redux/app_selector.dart";
3 | import "package:circles_app/domain/redux/app_state.dart";
4 | import "package:circles_app/model/channel.dart";
5 | import "package:redux/redux.dart";
6 |
7 | // ignore: prefer_double_quotes
8 | part 'main_screen_viewmodel.g.dart';
9 |
10 | abstract class MainScreenViewModel
11 | implements Built {
12 | bool get hasData;
13 |
14 | @nullable
15 | ChannelType get channelType;
16 |
17 | MainScreenViewModel._();
18 |
19 | factory MainScreenViewModel(
20 | [void Function(MainScreenViewModelBuilder) updates]) =
21 | _$MainScreenViewModel;
22 |
23 | static bool _hasData(Store store) {
24 | return store.state.user != null &&
25 | store.state.channelState.selectedChannel != null;
26 | }
27 |
28 | static MainScreenViewModel fromStore(Store store) {
29 | return MainScreenViewModel((vm) => vm
30 | ..hasData = _hasData(store)
31 | ..channelType = getSelectedChannel(store.state)?.type);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/lib/presentation/image/image_pinch_screen.dart:
--------------------------------------------------------------------------------
1 | import "package:flutter/material.dart";
2 | import "package:pinch_zoom_image/pinch_zoom_image.dart";
3 | import "package:transparent_image/transparent_image.dart";
4 |
5 | class ImagePinchScreen extends StatelessWidget {
6 |
7 | @override
8 | Widget build(BuildContext context) {
9 | final String url = ModalRoute.of(context).settings.arguments;
10 | return SafeArea(
11 | child: GestureDetector(
12 | child: PinchZoomImage(
13 | image: FadeInImage.memoryNetwork(
14 | image: url,
15 | placeholder: kTransparentImage,
16 | ),
17 | ),
18 | onTap: () {
19 | Navigator.pop(context);
20 | },
21 | ),
22 | );
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/lib/presentation/image/image_with_loader.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/theme.dart";
2 | import "package:flutter/material.dart";
3 | import "package:transparent_image/transparent_image.dart";
4 |
5 | class ImageWithLoader extends StatelessWidget {
6 | const ImageWithLoader({
7 | this.url,
8 | this.fit = BoxFit.cover,
9 | this.loaderSize = 48.0,
10 | });
11 |
12 | final String url;
13 | final BoxFit fit;
14 | final double loaderSize;
15 |
16 | @override
17 | Widget build(BuildContext context) {
18 | return Stack(
19 | fit: StackFit.expand,
20 | alignment: Alignment.center,
21 | children: [
22 | Container(
23 | color: AppTheme.colorGrey241,
24 | child: Center(
25 | child: SizedBox(
26 | width: loaderSize,
27 | height: loaderSize,
28 | child: _buildCircularProgressIndicator(),
29 | ),
30 | ),
31 | ),
32 | FadeInImage.memoryNetwork(
33 | image: url,
34 | fit: fit,
35 | placeholder: kTransparentImage,
36 | ),
37 | ],
38 | );
39 | }
40 | }
41 |
42 | CircularProgressIndicator _buildCircularProgressIndicator() {
43 | return CircularProgressIndicator(
44 | backgroundColor: AppTheme.colorGrey225,
45 | strokeWidth: 3,
46 | valueColor: AlwaysStoppedAnimation(AppTheme.colorGrey155),
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/lib/presentation/login/auth_button.dart:
--------------------------------------------------------------------------------
1 | import "package:flutter/material.dart";
2 | import "package:flutter/widgets.dart";
3 |
4 | class AuthButton extends StatelessWidget {
5 | final String buttonText;
6 | final Function onPressedCallback;
7 |
8 | const AuthButton({
9 | @required this.buttonText,
10 | this.onPressedCallback
11 | });
12 |
13 | @override
14 | Widget build(BuildContext context) {
15 | return RaisedButton(
16 | onPressed: onPressedCallback,
17 | color: Colors.blue,
18 | child: Container(
19 | height: 50.0,
20 | alignment: Alignment.center,
21 | child: Text(
22 | buttonText,
23 | textAlign: TextAlign.center,
24 | style: TextStyle(color: Colors.white)
25 | )
26 | ),
27 | );
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/lib/presentation/login/auth_button_container.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/domain/redux/app_state.dart";
2 | import "package:circles_app/circles_localization.dart";
3 | import "package:circles_app/domain/redux/authentication/auth_actions.dart";
4 | import "package:circles_app/presentation/login/auth_button.dart";
5 | import "package:flutter/cupertino.dart";
6 | import "package:flutter_redux/flutter_redux.dart";
7 | import "package:redux/redux.dart";
8 |
9 | class AuthButtonContainer extends StatelessWidget {
10 | const AuthButtonContainer();
11 |
12 | @override
13 | Widget build(BuildContext context) {
14 | return StoreConnector(
15 | converter: _ViewModel.fromStore,
16 | builder: (BuildContext context, _ViewModel viewModel) {
17 | return AuthButton(
18 | buttonText: viewModel.isLoggedIn ? CirclesLocalizations.of(context).logOut : CirclesLocalizations.of(context).logIn,
19 | onPressedCallback: viewModel.onPressedCallback
20 | );
21 | }
22 | );
23 | }
24 | }
25 |
26 | class _ViewModel {
27 | final bool isLoggedIn;
28 | final Function onPressedCallback;
29 |
30 | _ViewModel(this.isLoggedIn, this.onPressedCallback);
31 |
32 | static _ViewModel fromStore(Store store) {
33 | return _ViewModel(
34 | store.state.user != null,
35 | () {
36 | if (store.state.user != null) {
37 | store.dispatch(LogOutAction());
38 | } else {
39 | store.dispatch(LogIn());
40 | }
41 | }
42 | );
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/lib/presentation/settings/privacy_settings_button.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/circles_localization.dart";
2 | import "package:flutter/material.dart";
3 | import "package:url_launcher/url_launcher.dart";
4 |
5 | class PrivacySettingsButton extends StatelessWidget {
6 | const PrivacySettingsButton({
7 | Key key,
8 | }) : super(key: key);
9 |
10 | @override
11 | Widget build(BuildContext context) {
12 | return FlatButton(
13 | child: Text(CirclesLocalizations.of(context).privacyButton),
14 | onPressed: () {
15 | launch(CirclesLocalizations.of(context).privacyLink);
16 | },
17 | );
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/lib/presentation/settings/settings_screen.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/circles_localization.dart";
2 | import "package:circles_app/presentation/common/common_app_bar.dart";
3 | import "package:circles_app/presentation/settings/privacy_settings_button.dart";
4 | import "package:flutter/material.dart";
5 |
6 | class SettingsScreen extends StatelessWidget {
7 | @override
8 | Widget build(BuildContext context) {
9 | return Scaffold(
10 | appBar: CommonAppBar(
11 | title: CirclesLocalizations.of(context).settingsTitle,
12 | ),
13 | body: ListView(
14 | children: [
15 | PrivacySettingsButton(),
16 | ],
17 | ),
18 | );
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/lib/presentation/user/profile_avatar.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/model/user.dart";
2 | import "package:circles_app/presentation/user/user_avatar.dart";
3 | import "package:flutter/material.dart";
4 |
5 | class ProfileAvatar extends StatelessWidget {
6 | const ProfileAvatar({
7 | Key key,
8 | this.pictureIconButton,
9 | @required this.user,
10 | }) : super(key: key);
11 |
12 | final Widget pictureIconButton;
13 | final User user;
14 |
15 | @override
16 | Widget build(BuildContext context) {
17 | return Stack(
18 | alignment: Alignment.center,
19 | children: [
20 | SizedBox(
21 | width: _Style.avatarSize,
22 | child: Stack(
23 | alignment: Alignment.center,
24 | children: [
25 | CircularProgressIndicator(),
26 | UserAvatar(
27 | user: user,
28 | size: _Style.avatarSize,
29 | ),
30 | Positioned(
31 | bottom: 12,
32 | right: 12,
33 | child: AnimatedSwitcher(
34 | child: pictureIconButton ?? SizedBox.shrink(),
35 | duration: Duration(milliseconds: 200),
36 | ),
37 | )
38 | ],
39 | ),
40 | ),
41 | ],
42 | );
43 | }
44 | }
45 |
46 | class _Style {
47 | static const double avatarSize = 200.0;
48 | }
49 |
--------------------------------------------------------------------------------
/lib/presentation/user/rsvp_icon.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/model/channel.dart";
2 | import "package:flutter/material.dart";
3 | import "package:flutter/widgets.dart";
4 |
5 | class RsvpIcon extends StatelessWidget {
6 | final RSVP rsvp;
7 |
8 | const RsvpIcon({
9 | @required this.rsvp,
10 | });
11 |
12 | @override
13 | Widget build(BuildContext context) {
14 | final height = 24.0, width = 24.0;
15 | switch (rsvp) {
16 | case RSVP.YES:
17 | return Image.asset(
18 | "assets/graphics/channel/rsvp/rsvp_yes.png",
19 | height: height,
20 | width: width,
21 | );
22 | case RSVP.MAYBE:
23 | return Image.asset(
24 | "assets/graphics/channel/rsvp/rsvp_maybe.png",
25 | height: height,
26 | width: width,
27 | );
28 | case RSVP.NO:
29 | case RSVP.UNSET:
30 | default:
31 | return Container();
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/lib/presentation/user/selected_item.dart:
--------------------------------------------------------------------------------
1 | import "package:flutter/material.dart";
2 | import "package:flutter/widgets.dart";
3 |
4 | class SelectedItem extends StatelessWidget {
5 | final bool selected;
6 |
7 | const SelectedItem({
8 | this.selected,
9 | });
10 |
11 | @override
12 | Widget build(BuildContext context) {
13 | final height = 24.0, width = 24.0;
14 | return (selected
15 | ? Image.asset(
16 | "assets/graphics/input/checkbox_active.png",
17 | height: height,
18 | width: width,
19 | )
20 | : Image.asset(
21 | "assets/graphics/input/checkbox_inactive.png",
22 | height: height,
23 | width: width,
24 | ));
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/lib/presentation/user/user_avatar.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/model/user.dart";
2 | import "package:circles_app/theme.dart";
3 | import "package:flutter/material.dart";
4 | import "package:transparent_image/transparent_image.dart";
5 |
6 | class UserAvatar extends StatelessWidget {
7 | const UserAvatar({
8 | @required this.user,
9 | this.size = AppTheme.avatarSize,
10 | });
11 |
12 | // user can be null
13 | final User user;
14 | final double size;
15 |
16 | @override
17 | Widget build(BuildContext context) {
18 | if (user?.image == null) {
19 | return Image.asset(
20 | "assets/graphics/avatar_no_picture.png",
21 | height: size,
22 | width: size,
23 | fit: BoxFit.contain,
24 | );
25 | }
26 |
27 | return ClipRRect(
28 | borderRadius: BorderRadius.circular(8.0),
29 | child: FadeInImage.memoryNetwork(
30 | image: user.image,
31 | height: size,
32 | width: size,
33 | fit: BoxFit.fitHeight,
34 | placeholder: kTransparentImage,
35 | ),
36 | );
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/lib/presentation/user/user_screen_viewmodel.dart:
--------------------------------------------------------------------------------
1 | import "dart:async";
2 |
3 | import "package:built_value/built_value.dart";
4 | import "package:circles_app/domain/redux/app_state.dart";
5 | import "package:circles_app/domain/redux/user/user_actions.dart";
6 | import "package:circles_app/model/user.dart";
7 | import "package:redux/redux.dart";
8 |
9 | // ignore: prefer_double_quotes
10 | part 'user_screen_viewmodel.g.dart';
11 |
12 | abstract class UserScreenViewModel
13 | implements Built {
14 | User get user;
15 |
16 | bool get isYou;
17 |
18 | @BuiltValueField(compare: false)
19 | void Function(User user, Completer completer) get submit;
20 |
21 | UserScreenViewModel._();
22 |
23 | factory UserScreenViewModel(
24 | [void Function(UserScreenViewModelBuilder) updates]) =
25 | _$UserScreenViewModel;
26 |
27 | static fromStore(String userId) {
28 | return (Store store) {
29 | return UserScreenViewModel((u) => u
30 | ..user = _getUser(store, userId)
31 | ..isYou = userId == store.state.user.uid
32 | ..submit = (user, completer) =>
33 | store.dispatch(UpdateUserAction(user, completer)));
34 | };
35 | }
36 |
37 | static UserBuilder _getUser(Store store, String userId) {
38 | return store.state.groupUsers
39 | .firstWhere((user) => user.uid == userId)
40 | .toBuilder();
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/lib/routes.dart:
--------------------------------------------------------------------------------
1 | class Routes {
2 | static final home = "/";
3 | static final login = "/login";
4 | static final channelNew = "/channel/new";
5 | static final channelInvite = "/channel/invite";
6 | static final eventNew = "/event/new";
7 | static final image = "/image";
8 | static final imagePinch = "/image/pinch";
9 | static final imagePicker = "/image/picker";
10 | static final reaction = "/reaction";
11 | static final user = "/user";
12 | static final settings = "/settings";
13 | }
--------------------------------------------------------------------------------
/lib/util/HexColor.dart:
--------------------------------------------------------------------------------
1 | import "dart:ui";
2 |
3 | class HexColor extends Color {
4 | static int _getColorFromHex(String hexColor) {
5 | hexColor = hexColor.toUpperCase().replaceAll("#", "");
6 | if (hexColor.length == 6) {
7 | hexColor = "FF" + hexColor;
8 | }
9 | return int.parse(hexColor, radix: 16);
10 | }
11 |
12 | HexColor(final String hexColor) : super(_getColorFromHex(hexColor));
13 | }
--------------------------------------------------------------------------------
/lib/util/cache.dart:
--------------------------------------------------------------------------------
1 | import "dart:collection";
2 |
3 | /// Basic Cache based on first-in-first-out
4 | ///
5 | /// When the cache gets full (determined by the [size]) it will start
6 | /// removing the older values.
7 | ///
8 | /// Uses a simple [Queue] to store keys, and start removing the first ones
9 | /// in a "first in-first out" manner, until the queue is smaller than the max
10 | /// [size].
11 | ///
12 | /// This cache can be used for storing in memory thumbnails and similar.
13 | ///
14 | /// Call to [clear] when no longer used.
15 | ///
16 | class BasicCache {
17 | BasicCache({
18 | this.size,
19 | });
20 |
21 | final int size;
22 | final _map = Map();
23 | final _queue = Queue();
24 |
25 | bool containsKey(K key) {
26 | return _map.containsKey(key);
27 | }
28 |
29 | V operator [](K key) {
30 | return _map[key];
31 | }
32 |
33 | void operator []=(K key, V value) {
34 | _map[key] = value;
35 | _queue.add(key);
36 | _deleteOldValues();
37 | }
38 |
39 | void clear() {
40 | _map.clear();
41 | _queue.clear();
42 | }
43 |
44 | void _deleteOldValues() {
45 | while (_queue.length > size) {
46 | final key = _queue.removeFirst();
47 | _map.remove(key);
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/lib/util/date_formatting.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/util/logger.dart";
2 | import "package:flutter/widgets.dart";
3 | import "package:intl/intl.dart";
4 |
5 | String formatTime(BuildContext context, DateTime date) {
6 | try {
7 | DateFormat dateFormat;
8 | if (MediaQuery.of(context).alwaysUse24HourFormat) {
9 | dateFormat = DateFormat.Hm(Localizations.localeOf(context).languageCode);
10 | } else {
11 | dateFormat = DateFormat.jm(Localizations.localeOf(context).languageCode);
12 | }
13 | return dateFormat.format(date);
14 | } catch (error) {
15 | Logger.e("Error with time format: $error", e: error, s: StackTrace.current);
16 | return "";
17 | }
18 | }
19 |
20 | String formatDate(BuildContext context, DateTime date) {
21 | try {
22 | final datePattern = "EEE, MMM d";
23 | final dateFormat = DateFormat(datePattern, Localizations.localeOf(context).languageCode);
24 | return dateFormat.format(date);
25 | } catch (error) {
26 | Logger.e("Error with date format: $error", e: error, s: StackTrace.current);
27 | return "";
28 | }
29 | }
30 |
31 | String formatDateShort(BuildContext context, DateTime date) {
32 | try {
33 | final dateFormat = DateFormat.yMd(Localizations.localeOf(context).languageCode);
34 | return dateFormat.format(date);
35 | } catch (error) {
36 | Logger.e("Error with date format: $error", e: error, s: StackTrace.current);
37 | return "";
38 | }
39 | }
40 |
41 | String formatCalendarDate(BuildContext context, DateTime date) {
42 | try {
43 | final datePattern = "EEEE d. MMM";
44 | final dateFormat = DateFormat(datePattern, Localizations.localeOf(context).languageCode);
45 | return dateFormat.format(date);
46 | } catch (error) {
47 | Logger.e("Error with calendar date format: $error", e: error, s: StackTrace.current);
48 | return "";
49 | }
50 | }
--------------------------------------------------------------------------------
/lib/util/permissions.dart:
--------------------------------------------------------------------------------
1 | import "dart:io";
2 |
3 | import "package:circles_app/native_channels/android_permission_channel.dart";
4 | import "package:circles_app/native_channels/ios_permission_channel.dart";
5 | import "package:circles_app/util/logger.dart";
6 | import "package:flutter/services.dart";
7 |
8 | const MethodChannel channel = MethodChannel("de.janoodle.timy/permission");
9 |
10 | /// Obtain the status for storage/photos permissions
11 | ///
12 | /// If the app has no permission, it will request it.
13 | /// Uses [PermissionType.Photos] for iOS and Android.
14 | ///
15 | /// For other platforms always returns false.
16 | Future getStoragePermission() async {
17 | if (Platform.isIOS) {
18 | final status = await IOSPermissionChannel.requestPermission(
19 | permissionType: PermissionType.Photos);
20 | Logger.d("Photo permission status: $status");
21 | return (status == "AUTHORIZED") ? true : false;
22 | } else if (Platform.isAndroid) {
23 | final result = await AndroidPermissionChannel.requestPermission(
24 | permissionType: PermissionType.Photos);
25 | Logger.d("Photo permission status: $result");
26 | return result;
27 | }
28 | Logger.e("Invalid platform", s: StackTrace.current);
29 | return false;
30 | }
31 |
32 | /// Requests camera permission
33 | ///
34 | /// If the app has no permission, it will request it.
35 | /// Uses [PermissionType.Camera] for iOS.
36 | ///
37 | /// Android always returns true.
38 | ///
39 | /// For other platforms always returns false.
40 | Future getCameraPermission() async {
41 | if (Platform.isIOS) {
42 | final status = await IOSPermissionChannel.requestPermission(
43 | permissionType: PermissionType.Camera);
44 | Logger.w("Photo permission status: $status");
45 | return (status == "AUTHORIZED") ? true : false;
46 | } else if (Platform.isAndroid) {
47 | // Android does not need permissions to use camera as "Intent"
48 | return true;
49 | }
50 | Logger.e("Invalid platform", s: StackTrace.current);
51 | return false;
52 | }
53 |
--------------------------------------------------------------------------------
/res/values/strings_en.arb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/res/values/strings_en.arb
--------------------------------------------------------------------------------
/test/data/FirestoreMocks.dart:
--------------------------------------------------------------------------------
1 | import "package:cloud_firestore/cloud_firestore.dart";
2 | import "package:mockito/mockito.dart";
3 |
4 | class MockDocumentSnapshot extends Mock implements DocumentSnapshot {}
5 |
6 | class MockSnapshotMetadata extends Mock implements SnapshotMetadata {}
7 |
--------------------------------------------------------------------------------
/test/data/circle_repository_test.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/data/group_repository.dart";
2 | import "package:circles_app/model/group.dart";
3 | import "package:cloud_firestore/cloud_firestore.dart";
4 | import "package:flutter_test/flutter_test.dart";
5 | import "package:mockito/mockito.dart";
6 |
7 |
8 | class MockDocumentSnapshot extends Mock implements DocumentSnapshot {}
9 |
10 | main() {
11 | group("Circle Repository", () {
12 | final circle = Group((c) => c
13 | ..id = "ID"
14 | ..name = "CIRCLE"
15 | ..hexColor = "FFFFFF"
16 | ..abbreviation = "CI"
17 | );
18 |
19 | test("should map DocumentSnapshot to Circle", () {
20 | final document = MockDocumentSnapshot();
21 | when(document["name"]).thenReturn("CIRCLE");
22 | when(document.documentID).thenReturn("ID");
23 | when(document["color"]).thenReturn("FFFFFF");
24 | when(document["abbreviation"]).thenReturn("CI");
25 | final outCircle = GroupRepository.fromDoc(document);
26 | expect(outCircle, circle);
27 | });
28 | });
29 | }
30 |
--------------------------------------------------------------------------------
/test/data/data_mocks.dart:
--------------------------------------------------------------------------------
1 | import "package:built_collection/built_collection.dart";
2 | import "package:circles_app/model/channel.dart";
3 | import "package:circles_app/model/group.dart";
4 | import "package:circles_app/model/user.dart";
5 |
6 | final mockUser = User((u) => u
7 | ..uid = "userId"
8 | ..name = "name"
9 | ..email = "email");
10 |
11 | final mockChannelUser = ChannelUser((u) => u
12 | ..id = "userId"
13 | ..rsvp = RSVP.UNSET);
14 |
15 | final mockChannel = Channel((c) => c
16 | ..id = "channelId"
17 | ..name = "name"
18 | ..visibility = ChannelVisibility.OPEN
19 | ..type = ChannelType.EVENT
20 | ..startDate = DateTime(3000, 1, 1)
21 | ..hasUpdates = false
22 | ..users = ListBuilder([mockChannelUser]));
23 |
24 | final mockGroup = Group((g) => g
25 | ..id = "groupdId"
26 | ..name = "group"
27 | ..channels.replace({"channelId": mockChannel})
28 | ..abbreviation = "g"
29 | ..hexColor = ""
30 | ..image = "");
31 |
--------------------------------------------------------------------------------
/test/data/user_repository_test.dart:
--------------------------------------------------------------------------------
1 | import "package:built_collection/built_collection.dart";
2 | import "package:circles_app/data/user_repository.dart";
3 | import "package:circles_app/model/user.dart";
4 | import "package:flutter_test/flutter_test.dart";
5 | import "package:mockito/mockito.dart";
6 |
7 | import "FirestoreMocks.dart";
8 |
9 | main() {
10 | group("User Repository", () {
11 | final user = User((u) => u
12 | ..uid = "ID"
13 | ..name = "NAME"
14 | ..email = "EMAIL"
15 | ..unreadUpdates = MapBuilder({}));
16 |
17 | test("should map user to Map", () {
18 | final map = UserRepository.toMap(user);
19 | expect(map, {
20 | "uid": "ID",
21 | "name": "NAME",
22 | "email": "EMAIL",
23 | });
24 | });
25 |
26 | test("should map DocumentSnapshot to User", () {
27 | final document = MockDocumentSnapshot();
28 | when(document["name"]).thenReturn("NAME");
29 | when(document["email"]).thenReturn("EMAIL");
30 | when(document.documentID).thenReturn("ID");
31 | when(document["unreadUpdates"]).thenReturn({});
32 | final userFromDoc = UserRepository.fromDoc(document);
33 | expect(userFromDoc, user);
34 | });
35 | });
36 | }
37 |
--------------------------------------------------------------------------------
/test/domain/auth_reducer_test.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/domain/redux/app_reducer.dart";
2 | import "package:circles_app/domain/redux/app_state.dart";
3 | import "package:circles_app/domain/redux/authentication/auth_actions.dart";
4 | import "package:circles_app/model/user.dart";
5 | import "package:redux/redux.dart";
6 | import "package:test/test.dart";
7 |
8 | main() {
9 | group("State Reducer", () {
10 | Store _testStore;
11 | final _testUser = User((u) => u
12 | ..uid = "UID"
13 | ..name = "NAME"
14 | ..email = "EMAIL");
15 |
16 | setUp(() {
17 | _testStore = Store(appReducer, initialState: AppState.init());
18 | });
19 |
20 | test("should load user OnAuthenticated into store", () {
21 | expect(_testStore.state.user, null);
22 | _testStore.dispatch(OnAuthenticated(user: _testUser));
23 | expect(_testStore.state.user, _testUser);
24 | });
25 |
26 | test("should remove user OnLogoutSuccess from store", () {
27 | _testStore.dispatch(OnAuthenticated(user: _testUser));
28 | _testStore.dispatch(OnLogoutSuccess());
29 | expect(_testStore.state.user, null);
30 | });
31 | });
32 | }
33 |
--------------------------------------------------------------------------------
/test/domain/message/message_reducer_test.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/domain/redux/app_reducer.dart";
2 | import "package:circles_app/domain/redux/app_state.dart";
3 | import "package:circles_app/domain/redux/message/message_actions.dart";
4 | import "package:circles_app/model/message.dart";
5 | import "package:circles_app/model/user.dart";
6 | import "package:flutter_test/flutter_test.dart";
7 | import "package:redux/redux.dart";
8 |
9 | main() {
10 | group("Message Reducer", () {
11 | final user = User((u) => u
12 | ..uid = "ID"
13 | ..name = "NAME"
14 | ..email = "EMAIL");
15 | final message = Message((m) => m
16 | ..id = "ID"
17 | ..body = "BODY"
18 | ..authorId = "authorId");
19 |
20 | test("should update messages", () {
21 | final store = Store(
22 | appReducer,
23 | initialState:
24 | AppState.init().rebuild((a) => a..user = user.toBuilder()),
25 | );
26 | expect(store.state.messagesOnScreen.isEmpty, true);
27 | store.dispatch(UpdateAllMessages([message]));
28 | expect(store.state.messagesOnScreen, [message]);
29 | });
30 | });
31 | }
32 |
--------------------------------------------------------------------------------
/test/domain/push/push_reducer_test.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/domain/redux/app_reducer.dart";
2 | import "package:circles_app/domain/redux/app_state.dart";
3 | import "package:circles_app/domain/redux/push/push_actions.dart";
4 | import "package:circles_app/model/channel.dart";
5 | import "package:circles_app/model/in_app_notification.dart";
6 | import "package:flutter_test/flutter_test.dart";
7 | import "package:redux/redux.dart";
8 |
9 | main() {
10 | group("Push Reducer", () {
11 | final notification = InAppNotification((i) => i
12 | ..message = "HELLO"
13 | ..userName = "USERNAME"
14 | ..groupName = "CIRCLENAME"
15 | ..groupId = "GROUPID"
16 | ..channel.update((c) => c
17 | ..type = ChannelType.TOPIC
18 | ..name = "CHANNEL NAME"
19 | ..visibility = ChannelVisibility.OPEN));
20 |
21 | test("should update AppState with push notification", () {
22 | final store = Store(
23 | appReducer,
24 | initialState: AppState.init(),
25 | );
26 | expect(store.state.inAppNotification, null);
27 | store.dispatch(ShowPushNotificationAction(notification));
28 | expect(store.state.inAppNotification, notification);
29 | });
30 |
31 | test("should update AppState when push notification dismissed", () {
32 | final store = Store(
33 | appReducer,
34 | initialState: AppState.init()
35 | .rebuild((a) => a..inAppNotification = notification.toBuilder()),
36 | );
37 | expect(store.state.inAppNotification, notification);
38 | store.dispatch(OnPushNotificationDismissedAction());
39 | expect(store.state.inAppNotification, null);
40 | });
41 | });
42 | }
43 |
--------------------------------------------------------------------------------
/test/domain/redux_mocks.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/data/channel_repository.dart";
2 | import "package:circles_app/data/group_repository.dart";
3 | import "package:circles_app/data/file_repository.dart";
4 | import "package:circles_app/data/message_repository.dart";
5 | import "package:circles_app/data/user_repository.dart";
6 | import "package:circles_app/domain/redux/app_state.dart";
7 | import "package:firebase_messaging/firebase_messaging.dart";
8 | import "package:flutter/foundation.dart";
9 | import "package:mockito/mockito.dart";
10 | import "package:redux/redux.dart";
11 | import "package:flutter/widgets.dart" as w;
12 |
13 | class MockGroupRepository extends Mock implements GroupRepository {}
14 |
15 | class MockChannelsRepository extends Mock implements ChannelRepository {}
16 |
17 | class MockMessageRepository extends Mock implements MessageRepository {}
18 |
19 | class MockUserRepository extends Mock implements UserRepository {}
20 |
21 | class MockFileRepository extends Mock implements FileRepository {}
22 |
23 | class MockFirebaseMessaging extends Mock implements FirebaseMessaging {}
24 |
25 | class MockMiddleware extends Mock implements MiddlewareClass {}
26 |
27 | // ignore: must_be_immutable
28 | class MockGlobalKey extends Mock implements w.GlobalKey {}
29 |
30 | class MockNavigatorState extends Mock implements w.NavigatorState {
31 | @override
32 | // ignore: invalid_override_different_default_values_named
33 | String toString({DiagnosticLevel minLevel}) => "";
34 | }
35 |
--------------------------------------------------------------------------------
/test/domain/user/user_middleware_test.dart:
--------------------------------------------------------------------------------
1 | import "dart:async";
2 |
3 | import "package:circles_app/domain/redux/app_reducer.dart";
4 | import "package:circles_app/domain/redux/app_state.dart";
5 | import "package:circles_app/domain/redux/authentication/auth_actions.dart";
6 | import "package:circles_app/domain/redux/user/user_actions.dart";
7 | import "package:circles_app/domain/redux/user/user_middleware.dart";
8 | import "package:circles_app/model/user.dart";
9 | import "package:flutter_test/flutter_test.dart";
10 | import "package:matcher/matcher.dart";
11 | import "package:mockito/mockito.dart";
12 | import "package:redux/redux.dart";
13 |
14 | import "../redux_mocks.dart";
15 |
16 | main() {
17 | group("User Middleware", () {
18 | final userRepo = MockUserRepository();
19 | final captor = MockMiddleware();
20 | final user = User((u) => u
21 | ..uid = "ID"
22 | ..name = "NAME"
23 | ..email = "EMAIL");
24 | final store = Store(
25 | appReducer,
26 | initialState: AppState.init(),
27 | middleware: createUserMiddleware(userRepo)..add(captor),
28 | );
29 |
30 | test("Should update user", () {
31 | final controller = StreamController(sync: true);
32 | when(userRepo.getUserStream("ID")).thenAnswer((_) => controller.stream);
33 |
34 | store.dispatch(OnAuthenticated(user: user));
35 | controller.add(user);
36 |
37 | verify(
38 | captor.call(
39 | any,
40 | TypeMatcher(),
41 | any,
42 | ) as dynamic,
43 | );
44 |
45 | controller.close();
46 | });
47 | });
48 | }
49 |
--------------------------------------------------------------------------------
/test/domain/user/user_reducer_test.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/domain/redux/app_reducer.dart";
2 | import "package:circles_app/domain/redux/app_state.dart";
3 | import "package:circles_app/domain/redux/user/user_actions.dart";
4 | import "package:circles_app/model/user.dart";
5 | import "package:flutter_test/flutter_test.dart";
6 | import "package:redux/redux.dart";
7 |
8 | main() {
9 | group("User Reducer", () {
10 | // Ids of user and userOld are the same
11 | final user = User((u) => u
12 | ..uid = "userId"
13 | ..name = "name"
14 | ..image = "imageUrl"
15 | ..status = "myStatus"
16 | ..email = "my@example.com");
17 |
18 | final userOld = User((u) => u
19 | ..uid = "userId"
20 | ..name = "nameOld"
21 | ..image = "imageUrlOld"
22 | ..status = "myStatusOld"
23 | ..email = "my@example.com");
24 |
25 | test("should update user when state empty", () {
26 | final store = Store(
27 | appReducer,
28 | initialState: AppState.init(),
29 | );
30 |
31 | expect(store.state.user, null);
32 | expect(store.state.groupUsers.contains(user), false);
33 | store.dispatch(OnUserUpdateAction(user));
34 | expect(store.state.user, user);
35 | expect(store.state.groupUsers.contains(user), true);
36 | });
37 |
38 | test("should update user when state has data", () {
39 | final store = Store(
40 | appReducer,
41 | initialState: AppState.init(),
42 | );
43 |
44 | // Load old user first
45 | store.dispatch(OnUserUpdateAction(userOld));
46 | expect(store.state.user, userOld);
47 | expect(store.state.groupUsers.contains(userOld), true);
48 | expect(store.state.groupUsers.contains(user), false);
49 |
50 | // Update user (e.g. changes status or name)
51 | store.dispatch(OnUserUpdateAction(user));
52 | expect(store.state.user, user);
53 | expect(store.state.groupUsers.contains(userOld), false);
54 | expect(store.state.groupUsers.contains(user), true);
55 | });
56 | });
57 | }
58 |
--------------------------------------------------------------------------------
/test/model/message_test.dart:
--------------------------------------------------------------------------------
1 | import "package:built_collection/built_collection.dart";
2 | import "package:circles_app/model/message.dart";
3 | import "package:circles_app/model/reaction.dart";
4 | import "package:flutter_test/flutter_test.dart";
5 |
6 | main() {
7 | group("Message Model", () {
8 | final message = Message((m) => m
9 | ..body = ""
10 | ..authorId = "USERID"
11 | ..reactions = BuiltMap.of({
12 | "USER1": Reaction((r) => r
13 | ..emoji = "❤️"
14 | ..userId = "USERID"
15 | ..timestamp = DateTime.now()
16 | ..userName = "USERNAME"),
17 | "USER2": Reaction((r) => r
18 | ..emoji = "❤️"
19 | ..userId = "USERID"
20 | ..timestamp = DateTime.now()
21 | ..userName = "USERNAME"),
22 | "USER3": Reaction((r) => r
23 | ..emoji = "😂"
24 | ..userId = "USERID"
25 | ..timestamp = DateTime.now()
26 | ..userName = "USERNAME"),
27 | }).toBuilder());
28 |
29 | test("should count emoji in reactions", () {
30 | final reactions = message.reactionsCount();
31 | expect(reactions, {
32 | "😂": 1,
33 | "❤️": 2,
34 | });
35 | });
36 | });
37 | }
38 |
--------------------------------------------------------------------------------
/test/presentation/channel/invite/invite_to_channel_viewmodel_test.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/domain/redux/app_reducer.dart";
2 | import "package:circles_app/domain/redux/app_state.dart";
3 | import "package:circles_app/model/channel.dart";
4 | import "package:circles_app/model/group.dart";
5 | import "package:circles_app/model/user.dart";
6 | import "package:circles_app/presentation/channel/invite/invite_to_channel_viewmodel.dart";
7 | import "package:flutter_test/flutter_test.dart";
8 | import "package:redux/redux.dart";
9 |
10 | main() {
11 | group("Invite To Channel ViewModel", () {
12 | test("should list users that are not already in the channel", () {
13 | final user = User((u) => u
14 | ..uid = "user"
15 | ..email = "email"
16 | ..name = "name");
17 |
18 | // Group has three users: 1, 2, 3
19 | final user1 = user.rebuild((u) => u..uid = "user1");
20 | final user2 = user.rebuild((u) => u..uid = "user2");
21 | final user3 = user.rebuild((u) => u..uid = "user3");
22 |
23 | // User 1 and 2 are already in the channel
24 | final channelUser1 = ChannelUser((u) => u
25 | ..id = "user1"
26 | ..rsvp = RSVP.UNSET);
27 | final channelUser2 = ChannelUser((u) => u
28 | ..id = "user2"
29 | ..rsvp = RSVP.UNSET);
30 |
31 | final store = Store(appReducer,
32 | initialState: AppState.init().rebuild((s) => s
33 | ..groupUsers.replace([user1, user2, user3])
34 | ..selectedGroupId = "groupId"
35 | ..groups.replace({
36 | "groupId": Group((g) => g
37 | ..id = "groupId"
38 | ..name = "group"
39 | ..hexColor = ""
40 | ..abbreviation = ""
41 | ..channels.replace({
42 | "channelId": Channel((c) => c
43 | ..id = "channelId"
44 | ..name = "name"
45 | ..visibility = ChannelVisibility.CLOSED
46 | ..type = ChannelType.EVENT
47 | ..users.replace([channelUser1, channelUser2]))
48 | }))
49 | })));
50 |
51 | final vm = InviteToChannelViewModel.fromStore("channelId")(store);
52 |
53 | // Only user 3 should appear in the list
54 | expect(vm.newUsers, [user3]);
55 | });
56 | });
57 | }
58 |
--------------------------------------------------------------------------------
/test/presentation/reaction/reaction_details_test.dart:
--------------------------------------------------------------------------------
1 | import "package:built_collection/built_collection.dart";
2 | import "package:circles_app/model/reaction.dart";
3 | import "package:circles_app/presentation/channel/reaction/emoji_picker.dart";
4 | import "package:circles_app/presentation/channel/reaction/reaction_detail_data.dart";
5 | import "package:circles_app/presentation/channel/reaction/reaction_details.dart";
6 | import "package:flutter_test/flutter_test.dart";
7 |
8 | main() {
9 | group("Reaction details tests", () {
10 | test("should sort reaction details as expected", () {
11 | final map = BuiltMap({
12 | "USER2": Reaction((r) => r
13 | ..userId = "USERID"
14 | ..userName = "Miguel"
15 | ..timestamp = DateTime.now()
16 | ..emoji = emojiPickerOptions[2]),
17 | "USER4": Reaction((r) => r
18 | ..userId = "USERID"
19 | ..userName = "Lara"
20 | ..timestamp = DateTime(2019)
21 | ..emoji = emojiPickerOptions[2]),
22 | "USER1": Reaction((r) => r
23 | ..userId = "USERID"
24 | ..userName = "Lily"
25 | ..timestamp = DateTime.now()
26 | ..emoji = emojiPickerOptions[1]),
27 | "USER5": Reaction((r) => r
28 | ..userId = "USERID"
29 | ..userName = "Droid"
30 | ..timestamp = DateTime.now()
31 | ..emoji = emojiPickerOptions[0]),
32 | });
33 |
34 | final details = toListOfReactionDetailData(map);
35 |
36 | expect(details, [
37 | ReactionDetailData((r) => r
38 | ..emoji = emojiPickerOptions[2]
39 | ..names = "Miguel, Lara"),
40 | ReactionDetailData((r) => r
41 | ..emoji = emojiPickerOptions[0]
42 | ..names = "Droid"),
43 | ReactionDetailData((r) => r
44 | ..emoji = emojiPickerOptions[1]
45 | ..names = "Lily"),
46 | ]);
47 | });
48 | });
49 | }
50 |
--------------------------------------------------------------------------------
/test/synchronous_error.dart:
--------------------------------------------------------------------------------
1 | import "dart:async";
2 |
3 | class SynchronousError implements Future {
4 |
5 | final Object _error;
6 |
7 | SynchronousError(this._error);
8 |
9 | @override
10 | Stream asStream() {
11 | final StreamController controller = StreamController();
12 | controller.addError(_error);
13 | controller.close();
14 | return controller.stream;
15 | }
16 |
17 | @override
18 | Future catchError(Function onError, { bool test(dynamic error) }) {
19 | onError(_error);
20 | return Completer().future;
21 | }
22 |
23 | @override
24 | Future then(dynamic f(T value), { Function onError }) {
25 | return SynchronousError(_error);
26 | }
27 |
28 | @override
29 | Future timeout(Duration timeLimit, { dynamic onTimeout() }) {
30 | return Future.error(_error).timeout(timeLimit, onTimeout: onTimeout);
31 | }
32 |
33 | @override
34 | Future whenComplete(dynamic action()) {
35 | try {
36 | return this;
37 | } catch (e, stack) {
38 | return Future.error(e, stack);
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/test/widget_test.dart:
--------------------------------------------------------------------------------
1 | import "package:circles_app/circles_app.dart";
2 | import "package:flutter_test/flutter_test.dart";
3 |
4 | void main() {
5 | testWidgets("App loads test", (WidgetTester tester) async {
6 | // Build our app and trigger a frame.
7 | await tester.pumpWidget(CirclesApp());
8 |
9 | });
10 | }
11 |
--------------------------------------------------------------------------------
/timy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/janoodleFTW/timy-messenger/1442eb4a52169fb865f40a50fa1606b9499d6f4d/timy.png
--------------------------------------------------------------------------------