-
Notifications
You must be signed in to change notification settings - Fork 26
Home
- State management
- Pages
- Widgets
- Miscellaneous
State management in Twake Mobile is handled via Cubits, which in itself is a subset of the BLoC pattern. The library used for this purpose is Flutter BLoC. For the insertion of the Cubits into the widget tree, the application makes use of the GetX library.
All the data the application works with is defined in data models, so everything is typed as much as possible, with minimal amount of dynamic data types. There are top level models (for describing entities) and also there's satellite models to represent nested data structures.
The model contains only JWToken pair and their expiration timestamps. It's used to hold the current (most recent JWToken pair), which in turn is used in Twake API requests.
Fields:
Field name | Field type | Field description | Nullable? |
---|---|---|---|
token | String | Main JWToken used to authenticate user with Twake backend | false |
refreshToken | String | Token used to get a new JWToken after the current one has expired | false |
expiration | int | Timestamp, when main token expires | false |
refreshExpiration | int | Timestamp, when refresh token expires | false |
The model contains all the information about user (any user, not just the app user).
Fields:
Field name | Field type | Field description | Nullable? |
---|---|---|---|
id | String | User's unique identifier | false |
String | User's registered email | false | |
firstname | String | User's first name | true |
lastname | String | User's last name | true |
username | String | Username used in mentions | false |
thumbnail | String | Link to network resource from which user's avatar can be obtained | true |
consoleId | String | User's identifier in Twake Console management system | true |
statusIcon | String | Emoji code used as user's status icon | true |
status | String | Text describing user's current status | true |
language | String | User's preferred locale | true |
lastActivity | int | Last timestamp when user had some online activity | false |
The model contains all the information about a particular company, of which app user might have many.
Fields:
Field name | Field type | Field description | Nullable? |
---|---|---|---|
id | String | UUID like company's unique identifier | false |
name | String | Human readable name given to company | false |
logo | String | Link to network resource which points to logo image | true |
totalMembers | int | Number of collaborators invited to this company | false |
selectedWorkspace | String | Identifier of the last selected workspace in company | true |
permissions | [] | Permissions granted to app user to make changes in company | false |
The model contains all the information about a particular workspace, there could be many workspaces in single company.
List of fields in Workspace model:
Field name | Field type | Field description | Nullable? |
---|---|---|---|
id | String | UUID like workspace's unique identifier | false |
name | String | Human readable name given to workspace | false |
logo | String | Link to network resource which points to logo image | true |
companyId | String | Identifier of the parent company, to which workspace belongs | false |
totalMembers | int | Number of collaborators invited to this workspace | false |
userLastAccess | int | Timestamp value (in milliseconds), when the user last accessed the given workspace | false |
permissions | [] | Permissions granted to app user to make changes in given workspace | false |
The model contains all the information about a channel (public/private/direct), public and private channels belong to particular workspace, while direct channels belong to particular company.
Fields
Field name | Field type | Field description | Nullable? |
---|---|---|---|
id | String | Unique identifier of the given channel | false |
name | String | Human readable name of the channel | false |
icon | String | Emoji short name like :emoji:, used as channel icon, direct channels don't have one | true |
description | String | Human readable description of channel's purpose | true |
companyId | String | Unique identifier of parent company | false |
workspaceId | String | Unique identifier of parent workspace, direct channels have it set to 'direct' | false |
membersCount | int | Number of members, that the channel contains | false |
members | [] | List of members' identifiers, only direct channels have it non empty | false |
visibility | ChannelVisibility | Visibility type of the channel | false |
lastActivity | int | Timestamp (in milliseconds), of when the last event occured in channel | false |
lastMessage | MessageSummary | The last message sent to channel | true |
userLastAccess | int | Timestamp value (in milliseconds), when the user last accessed the given channel | false |
draft | String | Unsent string by user, autosaved for further use | true |
permissions | [] | Permissions granted to app user to make changes in a given channel | false |
Getters:
Getter | Type | Description |
---|---|---|
hasUnread | bool | Whether userLastAccess is less than lastActivity field |
membersCount | int | Amount of members in given channel |
hash | int | A unique value, which encodes the state of given channel, found by: name.hashCode + icon.hashCode + lastActivity + members.length |
The model contains all the information regarding a single message, message is agnostic of the fact where it was submitted: public, private, direct channel or in a thread. Thus the same model is used for messages everywhere.
Fields:
Field name | Field type | Field description | Nullable? |
---|---|---|---|
id | String | Unique identifier of the given message | false |
threadId | String | Identifier of the parent thread (if any) | true |
channelId | String | Identifier of the parent channel (public/private/direct) | false |
responsesCount | int | Number of responses to the message (only for channel level) | false |
userId | String | Identifier of user, who submitted the message, might be absent, if the message was sent by bot | false |
creationDate | int | Timestamp (in milliseconds), when the message was first submitted | false |
modificationDate | int | Timestamp (in milliseconds), when the message was edited, usually is the same as creationDate | false |
content | MessageContent | The content of the message | false |
reactions | Reaction[] | Reactions received by the given message from users | false |
username | String | Username of the the user, who submitted the message | false |
firstname | String | First name of the the user, who submitted the message | true |
lastname | String | Last name of the the user, who submitted the message | true |
thumbnail | String | Link to user's avatar, who submitted the message | true |
draft | String | Unsent string by user, autosaved for further use (only exists in parent messages) | true |
_isRead | int | binary value (int for easy storage in sqlite), indicates whether message is read by user or not, 1 by default | false |
Note: either userId or appId should be present
Getters:
Getter | Type | Description |
---|---|---|
hash | int | Pseudo unique hashCode of the message. See the calculation formula below |
sender | String | Human readable string, either user's firstname + lastname or username (if firstname is absent) |
isRead | String | wrapper around _isRead field, _isRead > 0 ? true : false |
Another note: hash field is calulated as the sum of properties of the following fields:
- hashCode of id
- hashCode of content.originalStr
- sum of hashCodes of names of each reaction in reactions field
- sum of count of each reaction in reactions field
Example:
final hash = id.hashCode +
content.originalStr.hashCode +
reactions.fold(0, (acc, r) => r.name.hashCode + acc) +
reactions.fold(0, (acc, r) => r.count + acc);
This class acts like big global variables storage, and keeps the following state.
List of getters/setters in Globals model:
Field name | Field type | Field description | Nullable? |
---|---|---|---|
host | String | Selected HOST (e.g. https://chat.twake.app) | false |
companyId | String | Selected company id | true |
workspaceId | String | Selected workspace id | true |
channelsType | ChannelsType | Which type of channel tab is selected on main screen: direct or public/private (default) | false |
channelId | String | Selected channel id | true |
threadId | String | Selected thread id | true |
token | String | Current token to access the Twake API | false |
fcmToken | String | Token obtained from Firebase Cloud Messaging | false |
userId | String | Identifier of the logged in user | false |
isNetworkConnected | bool | Current state of network connection | false |
Note: The model should be a globally accessable singleton. The class instance should be initialized before everything else, if the user has already logged into the app, in which case the previous state of the app should be available in local storage, otherwise the instance should be initialized before login by user with default data.
All the fields of the singleton instance should be kept up to date by other cubits, and the class itself is responsible for backing up all the changes to local storage
This model contains the information about files, either uploaded or the ones that can be downloaded.
Fields:
Field name | Field type | Field description | Nullable? |
---|---|---|---|
id | String | Unique identifier of the file in Twake storage | false |
name | String | Name of the file, as it stored in Twake storage | false |
preview | String | Relative link to preview image of the file | true |
download | String | Relative link to download the file | false |
size | int | The size of the file in bytes | false |
This model contains the information badge info: counters which are used to indicate the amount of unread messages on each level of hierarchy (company/workspace/channel)
Fields:
Field name | Field type | Field description | Nullable? |
---|---|---|---|
type | BadgeType | Level of hierarchy the counter belongs to | false |
id | String | Identifier of the entity on that level of hierarchy | false |
count | int | The number of unread messages, that given entity has | false |
Methods:
Method name | Arguments | Return type | Description |
---|---|---|---|
matches | BadgeType type, String id | bool | Returns true if the given Badge has its type and id equal to the arguments |
Getters:
Getter | Type | Description |
---|---|---|
hash | int | Value which encodes the current state of the Badge: type.hashCode + id.hashCode |
There two types of Notifications in Twake Mobile:
- FirebaseNotification - received via Firebase Cloud Messaging service, used for notifications about new messages. In order to communicate with Firebase Cloud Messaging the application makes use of the official plugin.
- LocalNotification - generated locally by the application itself. The plugin used for this purpose is Flutter local notification
Fields:
Field name | Field type | Field description | Nullable? |
---|---|---|---|
headers | NotificationHeaders | The information which is displayed in notification area of the device | false |
payload | NotificationPayload | The data, which is further used to retrieve the necessary message from API | false |
Fields:
Field name | Field type | Field description | Nullable? |
---|---|---|---|
type | LocalNotificationType | Discriminator field, used to distinguish different kinds of notification | false |
payload | Map<String, dynamic> | The key value store, which holds the dynamic payload, to be handle | false |
Getters:
Getter | Type | Description |
---|---|---|
stringified | String | Convenience method to convert the model state to json encoded string |
This section describes all the socketIO related data models which are received via socketIO channel, which is the primary way the application can be kept in realtime sync with the Twake server.
Fields:
Field name | Field type | Field description | Nullable? |
---|---|---|---|
name | String | Name of the socketIO room, from which the event was received | false |
data | MessageData | Payload of the event, which contains the data, regarding the modified message | false |
Example: |
{
name: previous::channels/2982dc0a-65aa-47ae-a13c-082b2e3cc2a9/messages/updates,
data: {
client_id: system,
action: update,
message_id: 5828c718-b49e-11eb-8ae0-0242ac120003,
thread_id: c920712e-b49d-11eb-8ed2-0242ac120003
}
}
Fields:
Field name | Field type | Field description | Nullable? |
---|---|---|---|
action | ResourceAction | Action that was performed on the resource | false |
type | ResourceType | Type of the resource, depending on it, the resource field is treated accordingly | false |
resource | Map<String, dynamic> | Data that should be updated in application | false |
Examples: Channel had some activity:
{
action: updated,
room: /companies/ac1a0544-1dcc-11eb-bf9f-0242ac120004/workspaces/ae6e73e8-504e-11eb-9a9c-0242ac120004/channels?type=public,
type: channel_activity,
path: /companies/ac1a0544-1dcc-11eb-bf9f-0242ac120004/workspaces/ae6e73e8-504e-11eb-9a9c-0242ac120004/channels/2982dc0a-65aa-47ae-a13c-082b2e3cc2a9,
resource: {
company_id: ac1a0544-1dcc-11eb-bf9f-0242ac120004,
workspace_id: ae6e73e8-504e-11eb-9a9c-0242ac120004,
id: 2982dc0a-65aa-47ae-a13c-082b2e3cc2a9,
last_activity: 1620987774000,
last_message: {
date: 1620987774000,
sender: 46a68a02-1dcc-11eb-95bd-0242ac120004,
sender_name: First Last,
title: 📄 ch1 in TestCompany • WS1, text: Message 2
}
}
}
Channel was created in workspace:
{
action: saved,
room: /companies/ac1a0544-1dcc-11eb-bf9f-0242ac120004/workspaces/ae6e73e8-504e-11eb-9a9c-0242ac120004/channels?type=public,
type: channel,
path: /companies/ac1a0544-1dcc-11eb-bf9f-0242ac120004/workspaces/ae6e73e8-504e-11eb-9a9c-0242ac120004/channels/2982dc0a-65aa-47ae-a13c-082b2e3cc2a9,
resource: {
is_default: false,
archived: false,
members: [],
connectors: [],
last_activity: 1620987956000,
company_id: ac1a0544-1dcc-11eb-bf9f-0242ac120004,
workspace_id: ae6e73e8-504e-11eb-9a9c-0242ac120004,
id: 2982dc0a-65aa-47ae-a13c-082b2e3cc2a9,
archivation_date: 0,
channel_group: ,
description: ,
icon: 📄,
name: Primary channel,
owner: 46a68a02-1dcc-11eb-95bd-0242ac120004,
visibility: public
}
}
Fields:
Field name | Field type | Field description | Nullable? |
---|---|---|---|
key | ResourceAction | Path, which is used to subscribe to room | false |
type | RoomType | Type of the room to subscribe to | false |
id | String | Id of the entity, the changes of which the application listens to | false |
subscribed | bool | Whether the room is currently subscribed to | false |
This enum describes all the possible values of that the visibility property of channel can take. Each field of this enum is JSON serializable/deserializable.
Possible values:
- public (JSON value 'public')
- private (JSON value 'private')
- direct (JSON value 'direct')
This a complementary model which is used to hold information about last message in a given channel. Also used to hold push notification data.
Fields:
Field name | Field type | Field description | Nullable? |
---|---|---|---|
date | int | Timestamp (in milliseconds), when the last message was submitted | false |
sender | String | Unique identifier of the user who submitted the message | false |
senderName | String | Human readable name of the sender, usually first name + last name | false |
title | String | Description of where the message was submitted (company, workspace, channel) | false |
text | String | Text content of the message | true |
Example JSON:
{
"date": 1621233242000,
"sender": "46a68a02-1dcc-11eb-95bd-0242ac120004",
"sender_name": "Firstname Lastname",
"title": "General in TestCompany • Analytics",
"text": "some text"
}
This is a complementary model used to hold the content of the message which is composed of 2 parts:
- The string input by the user
- Parsed structure of the input + some additional elements (e.g. attachments)
Fields:
Field name | Field type | Field description | Nullable? |
---|---|---|---|
originalStr | String | Text input by user, may be absent if the message contains only attachments | true |
prepared | List | Parsed structure of the message, which is the list of Strings, Maps and Lists | false |
Note: prepared field should be parsed by special parser which understands the syntax used by twake, in order to convert it to list of WidgetSpans, for later rendering.
Complementary model to hold reaction to a particular message.
Fields:
Field name | Field type | Field description | Nullable? |
---|---|---|---|
name | String | Emoji unicode (renderable) | false |
users | String[] | List of users' identifiers, who reacted with this emoji | false |
count | int | number of users reacted with this emoji | false |
This enum describes the current tab, that is selected on main screen. Each tab lists different types of channels: either directs or public/private (commons) Each field of this enum is JSON serializable/deserializable.
Possible values:
- commons (JSON value 'commons')
- directs (JSON value 'directs')
This enum describes the level at which the given badge counter holds the information about
Possible values:
- company (JSON value 'company')
- workspace (JSON value 'workspace')
- channel (JSON value 'channel')
Complementary model to hold the information, which is shown on notification area
Fields:
Field name | Field type | Field description | Nullable? |
---|---|---|---|
title | String | Title of the notification | false |
body | String | Content of the message | false |
Complementary model to hold the data regarding the new unread message
Fields:
Field name | Field type | Field description | Nullable? |
---|---|---|---|
companyId | String | Identifier of the company where new message was received | false |
workspaceId | String | Identifier of the workspace where new message was received | false |
channelId | String | Identifier of the channel where new message was received | false |
threadId | String | Identifier of the thread where new message was received | true |
messageId | String | Identifier of the new message itself | false |
Getters:
Getter | Type | Description |
---|---|---|
stringified | String | Convenience method to convert the model state to json encoded string |
This enum describes all the possible types that the local notification can be of. Each field of this enum is JSON serializable/deserializable. As the application evolves, new types might be added.
Possible values:
- message (JSON value 'message')
- file (JSON value 'file')
Complementary model to hold the data regarding the updated message, which is sent over socketIO channel.
Fields:
Field name | Field type | Field description | Nullable? |
---|---|---|---|
action | IOEventAction | Describes what action was performed on the message | false |
threadId | String | Identifier of the thread where the message was modified | false |
messageId | String | Identifier of the modified message itself | false |
Enum, which describes all the possible actions which can be performed over the message (socketIO channel specific)
Possible values:
- remove (JSON value 'remove')
- update (JSON value 'update')
Enum, which describes all the possible actions which can be performed over the resource (socketIO channel specific)
Possible values:
- updated (JSON value 'updated')
- saved (JSON value 'saved')
- deleted (JSON value 'deleted')
Enum, which describes all the possible resources that can be updated over the socketIO channel
Possible values:
- channel (JSON value 'channel')
- channelMember (JSON value 'channel_member')
- channelActivity (JSON value 'channel_activity')
For the purposes of building a reactive UI, the application makes use of various cubits, each of which is responsible for updating particular subset of data.
This cubit is pretty straightforward and has the following tasks to perform:
- authenticate user
- keep the JWTokens fresh
- keep the JWToken in Globals updated
- logout user from system, cleaning up entire local storage
Methods:
Name | Arguments | Return type | Description |
---|---|---|---|
authenticate | String username, String password | - | Try to authenticate with the provided credentials on Twake, update the state depending on result. On success start internal token validation |
checkAuthentication | - | - | Try to validate token (if present in local storage), update the state depending on result. On success start internal token validation |
logout | - | - | Update the state to initial, and cleanup the entire local storage |
States:
Name | Fields | Description |
---|---|---|
AuthenticationInitial | - | Initial state, which the cubit emits upon initialization |
AuthenticationInProgress | - | State emitted, when the authentication process starts |
AuthenticationFailure | String username, String password | State emitted, when authentication fails. It contains the entered username and password for user's convenience |
PostAuthenticationSyncInProgress | - | State emitted, when the user logs in the first time, and the application starts syncing user's data from the server |
PostAuthenticationSyncFailed | - | State emitted, when the user logs in the first time, and the data sync from twake server did not succeed |
AuthenticationSuccess | _ | State emitted when user successfully authenticateed or he/she already had an active token pair |
This cubit is responsible for 2 main features:
- Retrieve and save user information for the Profile screen.
- Fetching other users' available data for different purposes like search.
Methods:
Name | Arguments | Return type | Description |
---|---|---|---|
fetch | String? userId | - | Fetch the user by his id, first fetch the user from local storage, then make attempt to fetch from remote API. If userId is not provided, fetch current user |
fetchStateless | String userId | Account | Fetch the user from local storage and return it, without updating cubit's state |
States:
Name | Fields | Description |
---|---|---|
AccountInitial | - | The initial state, set during cubit's initialization |
AccountLoadInProgress | - | State, which is emitted when the account fetch has begun |
AccountLoadSuccess | Account account, int hash | State that is emitted when the account has been successfully retrieved from either remote or local storage |
AccountLoadFailure | String message | State that is emitted when a failure occured while attempting to retrieve the account from remote or local data source |
For now company management happens on console side, so this cubit is pretty simple. What it does:
- Fetch the list of available companies
- Manage companies selection
- Update Globals instance after selection
Methods:
Name | Arguments | Return type | Description |
---|---|---|---|
fetch | - | - | Fetch all the user's companies, first fetch from local storage, and then try to fetch from remote. |
selectCompany | String companyId | - | Update the cubit's state in such a way that the currently selected company is changed |
selectWorkspace | String workspaceId | - | For the selected company update its selectedWorkspace field, for the purposes of restoring state |
getSelectedCompany | - | Company? | Returns selected company in case if the companies are already loaded or null otherwise |
States:
Name | Fields | Description |
---|---|---|
CompaniesInitial | - | The initial state, set during cubit's initialization |
CompaniesLoadInProgress | - | State, which is emitted when the companies fetch has begun |
CompaniesLoadSuccess | Company[] companies, Company selected | State that is emitted when the companies has been successfully retrieved from either remote or local storage |
Task that are handled by this cubit:
- Fetching corresponding workspaces upon company selection
- Changing current workspace
- Update Globals accordingly
- Adding new workspaces (provided user has necessary permissions)
- Editing workspaces (including collaborators management)
- Removing workspace (provided user has necessary permissions)
It should be possible to switch workspace programmatically, most common case being user click on notification.
Methods:
Name | Arguments | Return type | Description |
---|---|---|---|
fetch | String companyId | - | Fetch all the workspaces in a given company, first fetch from local storage, and then try to fetch from remote. |
fetchMembers | String workspaceId | Account[] | Fetch all the collaborators in given workspace and return them |
selectWorkspace | String workspaceId | - | Update the cubit's state with the new selected workspace, also update Globals |
createWorkspace | String companyId, String name, String[] members | - | Try to create new workspace in a given company with given members, select created workspace and update cubit's state |
States:
Name | Fields | Description |
---|---|---|
WorkspacesInitial | - | The initial state, set during cubit's initialization |
WorkspacesLoadInProgress | - | State, which is emitted when the workspaces fetch has begun |
WorkspacesLoadSuccess | Workspace[] companies, Workspace selected | State that is emitted when the workspaces has been successfully retrieved from either remote or local storage |
This cubit should manage everything related to channels, both public and private.
Task performed by this cubit:
- Fetching the list of channels after workspace selection
- Channel selection, to open the list of messages in it
- Channel creation (public/private)
- Channel edition
- Channels deletion
Methods:
Name | Arguments | Return type | Description |
---|---|---|---|
fetch | String companyId, workspaceId | - | Fetch all the channels in a given company or workspace (depending on channel type), fetch from local storage, and then try to fetch from remote. |
create | String name, String icon, String description, ChannelVisibility visibility | bool | Try to create a channel, if successful return true, and update the current selected channel in cubit's state |
edit | Channel channel, String name, String icon, String description, ChannelVisibility visibility | bool | Try to edit a channel, if successful return true, and update the current selected channel in cubit's state |
delete | Channel channel | bool | Try to delete the channel, if successful return true, and update the current channels list in cubit's state |
fetchMembers | Channel channel | Account[] | Fetch all the members for the given channel |
addMembers | Channel channel, String[] usersToAdd | bool | Try to add given users to channel as members, if successful return true and update the cubit's state |
removeMembers | Channel channel, String[] usersToRemove | bool | Try to remove given users from channel's members list, if successful return true and update the cubit's state |
selectChannel | String channelId | - | Update the cubit's state with newly selected channel, and update Globals |
clearSelection | String channelId | - | Update the cubit's state removing selected channel, and update Globals |
listenToActivityChanges | - | - | Launches infinite loop, that listens to all the channel activity changes over socketIO channel, and on receiving any event, updates the cubit's state accordingly |
listenToChannelChanges | - | - | Launches infinite loop, that listens to all the channel changes over socketIO channel, and on receiving any event, updates the cubit's state accordingly |
States:
Name | Fields | Description |
---|---|---|
ChannelsInitial | - | The initial state, set during cubit's initialization |
ChannelsLoadInProgress | - | State, which is emitted when the channels fetch has begun |
ChannelsLoadSuccess | Channel[] companies, Channel selected | State that is emitted when the channels has been successfully retrieved from either remote or local storage |
This cubit should manage the screen of new channel creation
Task performed by this cubit:
- Change channel visibility
- Change channel icon
- Add member to channel
- Create new channel by calling Channels Cubit's method
This cubit should manage the screen adding and removing member from channel during its creation phase
Task performed by this cubit:
- Search members among workspace members
- Add members to channel
- Remove members from channel
This cubit should manage the screen of editing channel properties
Task performed by this cubit:
- Change channel visibility
- Start channel editing
This cubit should manage the screen of editing channel properties
Task performed by this cubit:
- Change channel name, description, icon
- Persist changes to backend
This cubit should manage the screen of editing existing channel's members list
Task performed by this cubit:
- Search members among workspace members
- Add new member to list
- Remove member to the list
- Persist changes to backend
This cubit should manage the screen of creating new direct chat with only one user
Task performed by this cubit:
- Search members among workspace members to start the chat with
- List out recent direct chats in a given company
- Create and persist new direct chat on backend
Same as Channels Cubit
Note: the socketIO streams in both cubits differ.
This cubit is one the most used ones, because it's responsible for managing messages.
Task performed by Messages Cubit:
- Loading the list of top levele messages in channel (public/private) or direct chats.
- Sending new messages
- Fetching messages on notification or socketIO event
- Editing the message
- Reacting to message
- Deleting message
Methods:
Name | Arguments | Return type | Description |
---|---|---|---|
fetch | String channelId, threadId | - | Fetch the messages in a given channel (and possibly filter by thread), fetch from local storage, and then try to fetch from remote. |
fetchBefore | String threadId | - | Fetch all previous messages in a given channel (and possibly filter by thread), messages that preceed (by timestamp) the first one in current cubit's state. Fetch from local storage, and then try to fetch from remote. |
send | String originalStr, File[] attachments, String threadId | - | Try to send the message, update the cubit's state immediately before making request to API (hoping for the best case scenario). After successful API response, update the cubit's state again with new message |
edit | Message message, String editedText, File[] newAttachments, String threadId | - | Try to update the message's content, update the cubit's state immediately before making request to API (hoping for the best case scenario) |
react | Message message, String reaction | - | Try to update message's reaction, update the cubit's state immediately before making API request. The API request can awaited and in case of failure, the reactions can be rolled back |
delete | Message message | - | Try to delete the message, update the cubit's state immediately before making API request. The API request can awaited and in case of failure, the message can be restored |
selectThread | String messageId | - | Update Globals with the selected thread identifier |
clearSelectedThread | - | - | Update Globals removing selectedThread identifier |
listenToMessageChanges | - | - | Launches infinite loop, that listens to all the message related events over socketIO channel, and on receiving any event, updates the cubit's state accordingly |
States:
Name | Fields | Description |
---|---|---|
MessagesInitial | - | The initial state, set during cubit's initialization |
MessagesLoadInProgress | - | State, which is emitted when the messages fetch has begun |
MessagesBeforeLoadInProgress | Message[] messages, int hash | State that is emitted when the previous messages fetch has begun |
MessagesLoadSuccess | Message[] messages, Message parentMessage, int hash | State that is emitted when the messages has been successfully retrieved from either remote or local storage. Same state is used both for channel messages and thread messages |
This cubit is responsible for continious synchronization of unread messages badges, which are in turn used in:
- List of companies
- List of workspaces
- List of channels (public/private/direct)
Those badges are basically numbers, which represent the amount of unread messages, which contain some form of mention, which user has subscribed to.
Methods:
Name | Arguments | Return type | Description |
---|---|---|---|
fetch | - | - | Fetch the all the badges for globally selected company, fetch from local storage, and then try to fetch from remote. |
listenToBadgeChanges | - | - | Launches infinite loop which listens to the socketIO events which are related to badge counter updates, and updates cubit's state accordingly |
States:
Name | Fields | Description |
---|---|---|
BadgesInitial | - | The initial state, set during cubit's initialization |
BadgesLoadInProgress | - | State, which is emitted when the badges fetch has begun |
BadgesLoadSuccess | Badge[] badges, int hash | State that is emitted when the badges has been successfully retrieved from either remote or local storage |
This cubit manages the list of users, which can be mentioned in given workspace.
Tasks performed by this cubit:
- It can provide that list filtered, based on the input given by user when typing the message
- It can parse any string, find all the substring which look like mentions, and run them against the list of users in given workspace, in order to find all the ids, which then (if found) can be attached to mentions. So what it does is it complements the mentions with users ids, for the subsequent use by twacode parser.
Methods:
Name | Arguments | Return type | Description |
---|---|---|---|
fetch | String search term | - | Fetch all the user from the selected workspace filtered by the searchTerm, update the cubit's state with the result |
reset | - | - | Reset the cubit's state to initial |
completeMentions | String messageText | String | Parses messageText, and check whether it includes any mentions, if it finds any, it tries to substitute all the mentions with mention + userId sequence if possible. And then it returns new string |
States:
Name | Fields | Description |
---|---|---|
MentionsInitial | - | The initial state, set during cubit's initialization |
MentionsLoadSuccess | Account[] account | State that is emitted, when accounts has been successfully retrieved from local storage, which satisfies searchTerm |
This cubit is responsible for keeping track of the files, that are being uploaded/downloaded to/from channel.
Task performed by this cubit:
- File upload (as multipart-form-data)
- File download (using Dio)
- Keeping the list of uploads before sending the message (act like an accumulator)
Methods:
Name | Arguments | Return type | Description |
---|---|---|---|
upload | String path | - | Upload the file from the provided path to channel, create File based on response, add the file to accumulator and update the cubit's state accordingly |
| download | File file | - | not implemented yet |
States:
Name | Fields | Description |
---|---|---|
FileInitial | - | The initial state, set during cubit's initialization |
FileUploadInProgress | CancelToken token, String name, int size | State that is emitted when file upload starts |
FileUploadFailed | String reason | State that is emitted when file upload failes |
FileUploadSuccess | File[] | State that is emitted when file has been successfully uploaded, contains all the files that has been uploaded so far |
FileDownloadInProgress | CancelToken token, File file | State that is emitted when file download starts |
FileDownloadFailed | String reason | State that is emitted when file download failes |
FileDownloadSuccess | String downloadPath | State that is emitted when file download is successfully complete, contains the path where the file has been downloaded |
This cubit is responsible for receiving media files from the device's albums their display and selection
Task performed by this cubit:
- Get Gallery Assets (photo_manager: ^)
- Tab Change
- Add Files Indexes
Methods:
Name | Arguments | Return type | Description |
---|---|---|---|
getGalleryAssets | bool isGettingNewAssets | - | get Gallery Assets from the device's albums |
tabChange | int tab | - | change selected Tab |
addFileIndex | int index | - | add file index which was selected |
States:
Name | Fields | Description |
---|---|---|
init | GalleryStateStatus galleryStateStatus, List assetsList, List assetEntity, List fileList, List selectedFilesIndex, int selectedTab, int loadedAssetsAmount, bool isAddingDummyAssets | The initial state, set during cubit's initialization |
loading | GalleryStateStatus galleryStateStatus, List assetsList, List assetEntity, List fileList, List selectedFilesIndex, int selectedTab, int loadedAssetsAmount, bool isAddingDummyAssets | State that is emitted when assets loading |
done | GalleryStateStatus galleryStateStatus, List assetsList, List assetEntity, List fileList, List selectedFilesIndex, int selectedTab, int loadedAssetsAmount, bool isAddingDummyAssets | State that is emitted when assets successfully loaded |
failed | GalleryStateStatus galleryStateStatus, List assetsList, List assetEntity, List fileList, List selectedFilesIndex, int selectedTab, int loadedAssetsAmount, bool isAddingDummyAssets | State that is emitted when an error occurred during the loading process |
newSelect | GalleryStateStatus galleryStateStatus, List assetsList, List assetEntity, List fileList, List selectedFilesIndex, int selectedTab, int loadedAssetsAmount, bool isAddingDummyAssets | State that is emitted when user select a new asset |
This сubit is responsible for caching files in channels and keeps data in the сubit state
Task performed by this cubit:
- Cache files
- Find cached file
- Clean cached files
Methods:
Name | Arguments | Return type | Description |
---|---|---|---|
cacheFile | File | - | add a file to cachedList |
findCachedFile | String fileId | File? | find cached file |
cleanCachedFiles | - | - | clean state |
This сubit is responsible for interacting with the device's camera
Task performed by this cubit:
- Select the main camera or switch it if that is needed
- Select flash modes
Methods:
Name | Arguments | Return type | Description |
---|---|---|---|
getCamera | - | - | Get the device's camera |
cameraLensSwitch | - | - | Switches between cameras |
nextFlashMode | - | - | Switches between the flash |
States:
Name | Fields | Description |
---|---|---|
init | final CameraStateStatus cameraStateStatus, final List availableCameras, final int flashMode, final bool cameraLensModeSwitch | The initial state, set during cubit's initialization |
loading | final CameraStateStatus cameraStateStatus, final List availableCameras, final int flashMode, final bool cameraLensModeSwitch | State that is emitted waiting for data to be received |
found | final CameraStateStatus cameraStateStatus, final List availableCameras, final int flashMode, final bool cameraLensModeSwitch | State that is emitted when cameras are detected |
done | final CameraStateStatus cameraStateStatus, final List availableCameras, final int flashMode, final bool cameraLensModeSwitch | State that is emitted when the camera is initialized |
failed | final CameraStateStatus cameraStateStatus, final List availableCameras, final int flashMode, final bool cameraLensModeSwitch | State that is emitted when initialization is failed |
All services in application are implemented as singletons, and must be initialized at startup. They can be accessed from anywhere in the app, once initialized, and they provide all the different kinds of data access mechanisms.
Main purpose of this service is to initialize other service before application starts and sync data with server on user's first successful login
Methods:
- preAuthenticationInit - initialize all the services that the application depends on, and create a Globals instance
- syncData - sequentially pull user's data from remote server and save it to local storage. The method should only be called once after successful login. Execution time might be long, dependind on the amount of data to sync.
This service is main entry point for communicating with Twake API. The ApiService class is implemented as a Singleton and like many other singletons can be accessed via instance static property.
The service is relatively simple, and acts as a very thin wrapper around Dio instance, which acts as HTTP client. The only somewhat complex logic handled by this service is done via Dio interceptors:
- It intercepts each request and completes the endpoint path with current host (accessed through Globals instance). After that it checks whether there's a need to add the authentication token to headers of the request, and adds one if necessary.
- It intercepts each error and sends error logs or messages to Sentry service, dependind on severity of the error.
Storage service provides very thin abstraction layer above sqflite package. Basically it only provides persistence mechanism only.
It's implemented as a class with a few static methods:
- insert - create a new entry in given table (replace if exists)
- multiInsert - create multiple entries in a given table (replace those which exist)
- select - query entries from given table, which satisfy given criteria
- first - light wrapper around select method, which returns the first element from result set
- rawSelect - last resort method, if you need to make complex query (with joins for ex.)
List of available tables:
- authentication
- account
- account2workspace (for relations between account and workspace)
- company
- workspace
- channel
- message
- badge
- globals
Each of the above given table is in one to one correspondence with Data models.
In order to reduce errors, service provides a special
Table enum, which holds all available table names. If you decide to add
another model, which requires persistency, then you have to also create another
entry in Table enum.
Example usage of methods:
- insert
final company = Company(id: "id", name: "name", logo: "http://logo.com"); await StorageService.instance.insert(Table.company, company);
- multiInsert
final companies = [ Company(id: "id1", name: "name1", logo: "http://logo.com"); Company(id: "id2", name: "name2", logo: "http://logo.com"); ]; await Storage.instance.multiInsert(Table.company, companies);
- select
await Storage.instance.select(Table.company, where: "id = ?", whereArgs: ["id1"]); await Storage.instance.select(Table.company, where: "name LIKE ?", whereArgs: ["Coca-Cola"]);
- first
await Storage.instance.first(Table.company, where: "id = ?", whereArgs: ["id1"]); await Storage.instance.first(Table.company, where: "name LIKE ?", whereArgs: ["Coca-Cola"]);
- rawSelect
final sql = 'SELECT u.* FROM user AS u JOIN user2workspace u2w ON u.id = u2w.user_id WHERE u2w.workspace_id = ?'; await Storage.instance.rawSelect(sql, args: ["id1"]);
This service moderately abstracts away the nitty-gritties of using Firebase Messaging service and Flutter Local Notifications Plugin.
It provides the following streams of events, that can be more conveniently handled by consumers of the service:
- foregroundMessageStream - Stream of messages from Firebase, which was received, while the application was in foreground
- notificationClickStream - Stream of messages from Firebase which was triggered by user click on corresponding notification, while application was in background
- localNotifications - Stream of LocalNotification messages, which was triggered by user clicks on corresponding local notifications
The service also has a method to request all the necessary permissions, in
order to be able to work with notifications requestPermission
.
Service provides to async getters which can be used to determine whether the app has been launched via notification click:
- checkRemoteNotificationClick - checks for remote (firebase) notification and returns notification body if present
- checkLocalNotificationClick - checks for local notification and returns the payload if present
In order to show local notification on demand, one can make use of showLocal
method, which will show the notification and returns the notification's id,
which further can be used to cancel pending notification if it's still in
notification area via cancelLocal
method.
This service is the primary and lowest level way to work with socketIO channels.
Main functionality provided by the service is subscriptions management to socketIO rooms:
- subscribe - method to start listening to events in particular room
- unsubsribe - method to stop listening to events in particular room Navigator Service
The service internally manages socketio connections health and automatically tries to reconnect after network loss events. Consumers of the service are responsible for resubscribing to rooms after network connection loss.
Service provides two streams which can be listened to:
- eventStream - socketIO events that relate to message changes
- resourceStream - socketIO events that relate to everything else (channels, members, etc)
Streams can be filtered and transformed by the consumers.
This service is a higher level abstraction over SocketIO Service and Push Notifications Service. It provides some conveniences, such as filtered socketio streams, autoresubscriptions, and management of foreground notifications
Streams:
- socketIODirectStream - stream of socketio resources that are related to direct chats (changing the icon, or the name)
- socketIODirectActivityStream - stream of socketio resources that are related to activity in direct chats (changing lastActivity)
- socketIOChannelStream - stream of socketio resources that are related to public or private channels
- socketIOChannelActivityStream - stream of socketio resources that are related to activity in public or private channels
- socketIOChannelMessageStream - stream of message related events in channels or direct chats
- socketIOThreadMessageStream - stream of message related events in threads (channels and direct chats alike)
Getters:
- socketIORooms - the list of available for subscription socketIO rooms, in a globally selected workspace
Methods:
- foregroundMessagesCheck - launches infinite loop which listenes to push notification events from firebase while the application is in the foreground. Once the notification is received it's converted to LocalNotification and shown to user if he/she's not already viewing the notification's channel.
- subscribeForChannels - convenience method to subscribe to rooms which are related to channel or direct chat related changes over socketio.
- subscribeToBadges - convenience method, to subscribe to badge counter changes over socketio
- cancelNotificationsForChannel - convenience method, to cancel (remove) all the pending local notifications which are related to a given channel
- subscribeToMessages - subscribes to message events on socketIO channel, which occur in a given channel
- unsubsribeFromMessages - cancels subscribtion to message events in a given channel
After reacquiring network connection, service will try to resubscribe to channels and badge counter update on socketio channel.
This service provides mechanisms to navigate to pages and update cubits' states (which the pages depend on) before navigation.
Upon initialization the service starts two loops, to check for notification clicks: one for firebase notifications, and one for local notifications, and navigates accordingly if the user click occurs.
Methods:
- navigateOnNotificationLaunch - method that should be called during app initialization. It checks whether notification click triggered application launch, and if so, navigates to appropriate page.
- listeneToLocals - loops over local notification click events, and performs navigation
- listeneToRemote - loops over firebase notification click events, and performs navigation
- navigate - heart of the service, used to navigate to required page
HTTP METHODS:
- POST - used to notify Twake backend to stop sending notifications to this device
POST /logout {
"fcm_token": <fcm_token>
}
HTTP METHODS:
- GET - get user by provided ID, if ID is not provided get user by JWT
GET /user {
"id": <user_id>
}
Expected result is Account compatible data structure in JSON format
HTTP METHODS:
- GET - get the list of all user's companies
GET /companies
Expected result is the list of Company compatible data structures in JSON format
HTTP METHODS:
- GET - get the list of all workspaces in a provided company
GET /workspaces {
"company_id": <company_id>
}
Expected result is the list of Workspace compatible data structures in JSON format
- POST - create new workspaces with provided data
POST /workspaces {
"company_id": <company_id>,
"logo": <need to provide id of uploaded file?>,
"name": <name>,
"members": <Array<string> emails>
}
Expected result is Workspace compatible object in JSON format
HTTP METHODS:
- GET - get all the members in a given workspace
GET /workspaces/members {
"company_id": <company_id>,
"workspace_id": <workspace_id>
}
Expected result is the list of Account compatible objects in JSON format
HTTP METHODS:
- GET - get all the unread message counters for workspaces and channels in a given company
GET /badges {
"company_id": <company_id>
}
Expected result is the list of Badge compatible objects in JSON format
HTTP METHODS:
- GET - get all the channels in a given workspace/company
GET /channels {
"company_id": <company_id>,
"workspace_id": <workspace_id>,
}
Expected result is the list of Channel compatible objects in JSON
- POST - create new channel with a given parameters
POST /channels {
"company_id": <company_id>,
"workspace_id": <workspace_id>,
"name": <name>,
"icon": <icon emoji>,
"description": <description>,
"members": <Array<string> ids>,
"visibility": <public | private>
}
Expected result is Channel compatible object of newly created channel
- PUT - edit given channel, updating provided parameters
PUT /channels {
"id": <channel_id>,
"company_id": <company_id>,
"workspace_id": <workspace_id>,
"name": <name>,
"icon": <icon emoji>,
"description": <description>,
"members": <Array<string> ids>,
"visibility": <public | private>
}
Expected result is Channel compatible object edited channel
- DELETE - delete given channel
DELETE /channels {
"id": <channel_id>,
"company_id": <company_id>,
"workspace_id": <workspace_id>
}
Expected result is boolean, regarding the status of operation
/direct - almost exactly the same as the endpoint for channels, but this one works with direct chats
HTTP METHODS:
- GET - get all the directs in a given company
GET /direct {
"company_id": <company_id>,
}
Expected result is the list of Channel compatible objects in JSON
- POST - create new direct with a given user
POST /direct {
"company_id": <company_id>,
"member": <id of user>,
}
Expected result is the list of Channel compatible objects in JSON
HTTP METHODS:
- POST - mark the channel as read, resetting unread messages counter and unread flag
POST /channels/read {
"id": <channel_id>,
"company_id": <company_id>,
"workspace_id": <workspace_id>
}
Expected result is boolean, regarding the status of operation
HTTP METHODS:
- POST - add new members to channel
POST /channels/members {
"company_id": <company_id>,
"workspace_id": <workspace_id>,
"id": <channel_id>,
"members": <Array<string> userids>
}
Expected result is boolean, regarding the status of operation
- DELETE - remove given members from channel
DELETE /channels/members {
"company_id": <company_id>,
"workspace_id": <workspace_id>,
"id": <channel_id>,
"members": <Array<string> userids>
}
Expected result is boolean, regarding the status of operation
HTTP METHODS:
- GET - get messages in a given channel or thread. Depending on parameters it should behave differently:
- if
thread_id
IS NOT passed, it should only return top level channel messages - if
thread_id
IS passed, then it should return only the messages for the given thread - if
id
is passed, then it should only return one single message with given id - if
after_date
is passed, then it should return only messages (either top level or thread level) after the given date (if any)
- if
GET /messages {
"company_id": <company_id>,
"workspace_id": <workspace_id | direct>,
"channel_id": <channel_id>,
"thread_id": <thread_id>,
"id": <message_id>,
"after_date": <timestamp>,
"limit": <integer>
}
Expected result is the list of Message compatible objects in JSON
- POST - create new message in a given channel
POST /messages {
"company_id": <company_id>,
"workspace_id": <workspace_id>,
"channel_id": <channel_id>,
"thread_id": <thread_id>,
"original_str": <message input>,
"prepared": <JSON structure of parsed message>
}
Expected result is Message compatible JSON object
- PUT - update the contents of given message
PUT /messages {
"company_id": <company_id>,
"workspace_id": <workspace_id>,
"channel_id": <channel_id>,
"thread_id": <thread_id>,
"message_id": <message_id>,
"original_str": <message input>,
"prepared": <JSON structure of parsed message>
}
Expected result is Message compatible JSON object
- DELETE - delete the given message
DELETE /messages {
"company_id": <company_id>,
"workspace_id": <workspace_id>,
"channel_id": <channel_id>,
"thread_id": <thread_id>,
"message_id": <message_id>
}
Expected result is boolean, regarding the status of operation
HTTP METHODS:
- POST - update the reactions list for a given message
POST /reactions {
"company_id": <company_id>,
"workspace_id": <workspace_id>,
"channel_id": <channel_id>,
"thread_id": <thread_id>,
"message_id": <message_id>,
"reaction": <emoji in unicode>,
}
Expected result is boolean, regarding the status of operation
/workspaces/notifications - endpoint for getting all the socket io rooms, that can be joined in a given workspace
HTTP METHODS:
- GET - get all the sockeio rooms in a given workspace
GET /workspace/notifications {
"company_id": <company_id>,
"workspace_id": <workspace_id>
}
Expected result is the list of SocketIORoom compatible objects in JSON