-
Notifications
You must be signed in to change notification settings - Fork 27
Home
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 |
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 |
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 |