-
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 listenes 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 eithere remote or local storage |