diff --git a/Backend/Api/Auth/ChangeMessageExpiry.go b/Backend/Api/Auth/ChangeUserMessageExpiry.go similarity index 88% rename from Backend/Api/Auth/ChangeMessageExpiry.go rename to Backend/Api/Auth/ChangeUserMessageExpiry.go index aa2fd5e..4805519 100644 --- a/Backend/Api/Auth/ChangeMessageExpiry.go +++ b/Backend/Api/Auth/ChangeUserMessageExpiry.go @@ -13,8 +13,8 @@ type rawChangeMessageExpiry struct { MessageExpiry string `json:"message_expiry"` } -// ChangeMessageExpiry handles changing default message expiry for user -func ChangeMessageExpiry(w http.ResponseWriter, r *http.Request) { +// ChangeUserMessageExpiry handles changing default message expiry for user +func ChangeUserMessageExpiry(w http.ResponseWriter, r *http.Request) { var ( user Models.User changeMessageExpiry rawChangeMessageExpiry diff --git a/Backend/Api/Auth/ChangeMessageExpiry_test.go b/Backend/Api/Auth/ChangeUserMessageExpiry_test.go similarity index 97% rename from Backend/Api/Auth/ChangeMessageExpiry_test.go rename to Backend/Api/Auth/ChangeUserMessageExpiry_test.go index 2c48c75..ca13511 100644 --- a/Backend/Api/Auth/ChangeMessageExpiry_test.go +++ b/Backend/Api/Auth/ChangeUserMessageExpiry_test.go @@ -10,7 +10,7 @@ import ( "git.tovijaeschke.xyz/tovi/Capsule/Backend/Tests" ) -func Test_ChangeMessageExpiry(t *testing.T) { +func Test_ChangeUserMessageExpiry(t *testing.T) { client, ts, err := Tests.InitTestEnv() defer ts.Close() if err != nil { diff --git a/Backend/Api/Messages/ChangeConversationMessageExpiry.go b/Backend/Api/Messages/ChangeConversationMessageExpiry.go new file mode 100644 index 0000000..b589590 --- /dev/null +++ b/Backend/Api/Messages/ChangeConversationMessageExpiry.go @@ -0,0 +1,74 @@ +package Messages + +import ( + "encoding/json" + "io/ioutil" + "net/http" + + "git.tovijaeschke.xyz/tovi/Capsule/Backend/Database" + "git.tovijaeschke.xyz/tovi/Capsule/Backend/Models" + "github.com/gorilla/mux" +) + +type rawChangeMessageExpiry struct { + MessageExpiry string `json:"message_expiry"` +} + +// ChangeUserMessageExpiry handles changing default message expiry for user +func ChangeConversationMessageExpiry(w http.ResponseWriter, r *http.Request) { + var ( + // user Models.User + changeMessageExpiry rawChangeMessageExpiry + conversationDetail Models.ConversationDetail + requestBody []byte + urlVars map[string]string + detailID string + ok bool + err error + ) + + urlVars = mux.Vars(r) + detailID, ok = urlVars["detailID"] + if !ok { + http.Error(w, "Not Found", http.StatusNotFound) + return + } + + conversationDetail, err = Database.GetConversationDetailByID(detailID) + if err != nil { + http.Error(w, "Not Found", http.StatusNotFound) + return + } + + // Ignore error here, as middleware should handle auth + // TODO: Check if user in conversation + // user, _ = Auth.CheckCookieCurrentUser(w, r) + + requestBody, err = ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, "Error", http.StatusInternalServerError) + return + } + + err = json.Unmarshal(requestBody, &changeMessageExpiry) + if err != nil { + http.Error(w, "Error", http.StatusInternalServerError) + return + } + + err = conversationDetail.MessageExpiryDefault.Scan(changeMessageExpiry.MessageExpiry) + if err != nil { + http.Error(w, "Error", http.StatusUnprocessableEntity) + return + } + + err = Database.UpdateConversationDetail( + &conversationDetail, + ) + if err != nil { + http.Error(w, "Error", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/Backend/Api/Messages/Conversations.go b/Backend/Api/Messages/Conversations.go index 1639111..03da54e 100644 --- a/Backend/Api/Messages/Conversations.go +++ b/Backend/Api/Messages/Conversations.go @@ -1,6 +1,7 @@ package Messages import ( + "database/sql/driver" "encoding/json" "net/http" "net/url" @@ -58,6 +59,7 @@ func ConversationDetailsList(w http.ResponseWriter, r *http.Request) { detail Models.ConversationDetail query url.Values conversationIds []string + messageExpiryRaw driver.Value returnJSON []byte i int ok bool @@ -82,6 +84,9 @@ func ConversationDetailsList(w http.ResponseWriter, r *http.Request) { } for i, detail = range conversationDetails { + messageExpiryRaw, _ = detail.MessageExpiryDefault.Value() + conversationDetails[i].MessageExpiry, _ = messageExpiryRaw.(string) + if detail.AttachmentID == nil { continue } diff --git a/Backend/Api/Routes.go b/Backend/Api/Routes.go index 73d98f4..5fd7ab6 100644 --- a/Backend/Api/Routes.go +++ b/Backend/Api/Routes.go @@ -63,7 +63,7 @@ func InitAPIEndpoints(router *mux.Router) { authAPI.HandleFunc("/check", Auth.Check).Methods("GET") authAPI.HandleFunc("/change_password", Auth.ChangePassword).Methods("POST") - authAPI.HandleFunc("/message_expiry", Auth.ChangeMessageExpiry).Methods("POST") + authAPI.HandleFunc("/message_expiry", Auth.ChangeUserMessageExpiry).Methods("POST") authAPI.HandleFunc("/image", Auth.AddProfileImage).Methods("POST") authAPI.HandleFunc("/users", Users.SearchUsers).Methods("GET") @@ -79,6 +79,7 @@ func InitAPIEndpoints(router *mux.Router) { authAPI.HandleFunc("/conversations", Messages.CreateConversation).Methods("POST") authAPI.HandleFunc("/conversations", Messages.UpdateConversation).Methods("PUT") authAPI.HandleFunc("/conversations/{detailID}/image", Messages.AddConversationImage).Methods("POST") + authAPI.HandleFunc("/conversations/{detailID}/message_expiry", Messages.ChangeConversationMessageExpiry).Methods("POST") authAPI.HandleFunc("/message", Messages.CreateMessage).Methods("POST") authAPI.HandleFunc("/messages/{associationKey}", Messages.Messages).Methods("GET") diff --git a/Backend/Database/ConversationDetails.go b/Backend/Database/ConversationDetails.go index 811fa62..7de9fe5 100644 --- a/Backend/Database/ConversationDetails.go +++ b/Backend/Database/ConversationDetails.go @@ -10,51 +10,51 @@ import ( // GetConversationDetailByID gets by id func GetConversationDetailByID(id string) (Models.ConversationDetail, error) { var ( - messageThread Models.ConversationDetail - err error + conversationDetail Models.ConversationDetail + err error ) err = DB.Preload(clause.Associations). Where("id = ?", id). - First(&messageThread). + First(&conversationDetail). Error - return messageThread, err + return conversationDetail, err } // GetConversationDetailsByIds gets by multiple ids func GetConversationDetailsByIds(id []string) ([]Models.ConversationDetail, error) { var ( - messageThread []Models.ConversationDetail - err error + conversationDetail []Models.ConversationDetail + err error ) err = DB.Preload(clause.Associations). Where("id IN ?", id). - Find(&messageThread). + Find(&conversationDetail). Error - return messageThread, err + return conversationDetail, err } // CreateConversationDetail creates a ConversationDetail record -func CreateConversationDetail(messageThread *Models.ConversationDetail) error { +func CreateConversationDetail(conversationDetail *Models.ConversationDetail) error { return DB.Session(&gorm.Session{FullSaveAssociations: true}). - Create(messageThread). + Create(conversationDetail). Error } // UpdateConversationDetail updates a ConversationDetail record -func UpdateConversationDetail(messageThread *Models.ConversationDetail) error { +func UpdateConversationDetail(conversationDetail *Models.ConversationDetail) error { return DB.Session(&gorm.Session{FullSaveAssociations: true}). - Where("id = ?", messageThread.ID). - Updates(messageThread). + Where("id = ?", conversationDetail.ID). + Updates(conversationDetail). Error } // DeleteConversationDetail deletes a ConversationDetail record -func DeleteConversationDetail(messageThread *Models.ConversationDetail) error { +func DeleteConversationDetail(conversationDetail *Models.ConversationDetail) error { return DB.Session(&gorm.Session{FullSaveAssociations: true}). - Delete(messageThread). + Delete(conversationDetail). Error } diff --git a/Backend/Models/Conversations.go b/Backend/Models/Conversations.go index 6df37ec..bc6ff3a 100644 --- a/Backend/Models/Conversations.go +++ b/Backend/Models/Conversations.go @@ -9,11 +9,13 @@ import ( // ConversationDetail stores the name for the conversation type ConversationDetail struct { Base - Name string `gorm:"not null" json:"name"` // Stored encrypted - Users []ConversationDetailUser ` json:"users"` - TwoUser string `gorm:"not null" json:"two_user"` - AttachmentID *uuid.UUID ` json:"attachment_id"` - Attachment Attachment ` json:"attachment"` + Name string `gorm:"not null" json:"name"` // Stored encrypted + Users []ConversationDetailUser ` json:"users"` + TwoUser string `gorm:"not null" json:"two_user"` + AttachmentID *uuid.UUID ` json:"attachment_id"` + Attachment Attachment ` json:"attachment"` + MessageExpiryDefault MessageExpiry `gorm:"default:no_expiry" json:"-" sql:"type:ENUM('fifteen_min', 'thirty_min', 'one_hour', 'three_hour', 'six_hour', 'twelve_hour', 'one_day', 'three_day', 'no_expiry')"` // Stored encrypted + MessageExpiry string `gorm:"-" json:"message_expiry"` // Stored encrypted } // ConversationDetailUser all users associated with a customer diff --git a/Backend/Models/MessageExpiry.go b/Backend/Models/MessageExpiry.go new file mode 100644 index 0000000..4d25ccc --- /dev/null +++ b/Backend/Models/MessageExpiry.go @@ -0,0 +1,70 @@ +package Models + +import ( + "database/sql/driver" + "errors" +) + +// MessageExpiry holds values for how long messages should expire by default +type MessageExpiry []uint8 + +const ( + // MessageExpiryFifteenMin expires after 15 minutes + MessageExpiryFifteenMin = "fifteen_min" + // MessageExpiryThirtyMin expires after 30 minutes + MessageExpiryThirtyMin = "thirty_min" + // MessageExpiryOneHour expires after one hour + MessageExpiryOneHour = "one_hour" + // MessageExpiryThreeHour expires after three hours + MessageExpiryThreeHour = "three_hour" + // MessageExpirySixHour expires after six hours + MessageExpirySixHour = "six_hour" + // MessageExpiryTwelveHour expires after twelve hours + MessageExpiryTwelveHour = "twelve_hour" + // MessageExpiryOneDay expires after one day + MessageExpiryOneDay = "one_day" + // MessageExpiryThreeDay expires after three days + MessageExpiryThreeDay = "three_day" + // MessageExpiryNoExpiry never expires + MessageExpiryNoExpiry = "no_expiry" +) + +// MessageExpiryValues list of all expiry values for validation +var MessageExpiryValues = []string{ + MessageExpiryFifteenMin, + MessageExpiryThirtyMin, + MessageExpiryOneHour, + MessageExpiryThreeHour, + MessageExpirySixHour, + MessageExpiryTwelveHour, + MessageExpiryOneDay, + MessageExpiryThreeDay, + MessageExpiryNoExpiry, +} + +// Scan new value into MessageExpiry +func (e *MessageExpiry) Scan(value interface{}) error { + var ( + strValue = value.(string) + m string + ) + + for _, m = range MessageExpiryValues { + if strValue != m { + continue + } + *e = MessageExpiry(strValue) + return nil + } + + return errors.New("Invalid MessageExpiry value") +} + +// Value gets value out of MessageExpiry column +func (e MessageExpiry) Value() (driver.Value, error) { + return string(e), nil +} + +func (e MessageExpiry) String() string { + return string(e) +} diff --git a/Backend/Models/Users.go b/Backend/Models/Users.go index 736289e..fa587d4 100644 --- a/Backend/Models/Users.go +++ b/Backend/Models/Users.go @@ -1,84 +1,20 @@ package Models import ( - "database/sql/driver" - "errors" - "github.com/gofrs/uuid" "gorm.io/gorm" ) -// BeforeUpdate prevents updating the email if it has not changed +// BeforeUpdate prevents updating the username or email if it has not changed // This stops a unique constraint error func (u *User) BeforeUpdate(tx *gorm.DB) (err error) { if !tx.Statement.Changed("Username") { tx.Statement.Omit("Username") } - return nil -} - -// MessageExpiry holds values for how long messages should expire by default -type MessageExpiry []uint8 - -const ( - // MessageExpiryFifteenMin expires after 15 minutes - MessageExpiryFifteenMin = "fifteen_min" - // MessageExpiryThirtyMin expires after 30 minutes - MessageExpiryThirtyMin = "thirty_min" - // MessageExpiryOneHour expires after one hour - MessageExpiryOneHour = "one_hour" - // MessageExpiryThreeHour expires after three hours - MessageExpiryThreeHour = "three_hour" - // MessageExpirySixHour expires after six hours - MessageExpirySixHour = "six_hour" - // MessageExpiryTwelveHour expires after twelve hours - MessageExpiryTwelveHour = "twelve_hour" - // MessageExpiryOneDay expires after one day - MessageExpiryOneDay = "one_day" - // MessageExpiryThreeDay expires after three days - MessageExpiryThreeDay = "three_day" - // MessageExpiryNoExpiry never expires - MessageExpiryNoExpiry = "no_expiry" -) - -// MessageExpiryValues list of all expiry values for validation -var MessageExpiryValues = []string{ - MessageExpiryFifteenMin, - MessageExpiryThirtyMin, - MessageExpiryOneHour, - MessageExpiryThreeHour, - MessageExpirySixHour, - MessageExpiryTwelveHour, - MessageExpiryOneDay, - MessageExpiryThreeDay, - MessageExpiryNoExpiry, -} - -// Scan new value into MessageExpiry -func (e *MessageExpiry) Scan(value interface{}) error { - var ( - strValue = value.(string) - m string - ) - - for _, m = range MessageExpiryValues { - if strValue != m { - continue - } - *e = MessageExpiry(strValue) - return nil + if !tx.Statement.Changed("Email") { + tx.Statement.Omit("Email") } - - return errors.New("Invalid MessageExpiry value") -} - -// Value gets value out of MessageExpiry column -func (e MessageExpiry) Value() (driver.Value, error) { - return string(e), nil -} - -func (e MessageExpiry) String() string { - return string(e) + return nil } // User holds user data @@ -87,19 +23,11 @@ type User struct { Username string `gorm:"not null;unique" json:"username"` Password string `gorm:"not null" json:"password"` ConfirmPassword string `gorm:"-" json:"confirm_password"` + Email string ` json:"email"` AsymmetricPrivateKey string `gorm:"not null" json:"asymmetric_private_key"` // Stored encrypted AsymmetricPublicKey string `gorm:"not null" json:"asymmetric_public_key"` SymmetricKey string `gorm:"not null" json:"symmetric_key"` // Stored encrypted AttachmentID *uuid.UUID ` json:"attachment_id"` Attachment Attachment ` json:"attachment"` - MessageExpiryDefault MessageExpiry `gorm:"default:no_expiry" json:"-" sql:"type:ENUM( - 'fifteen_min', - 'thirty_min', - 'one_hour', - 'three_hour', - 'six_hour', - 'twelve_hour', - 'one_day', - 'three_day' - )"` // Stored encrypted + MessageExpiryDefault MessageExpiry `gorm:"default:no_expiry" json:"-" sql:"type:ENUM('fifteen_min', 'thirty_min', 'one_hour', 'three_hour', 'six_hour', 'twelve_hour', 'one_day', 'three_day', 'no_expiry')"` // Stored encrypted } diff --git a/Backend/main b/Backend/main index f4bdfab..8bb2fa5 100755 Binary files a/Backend/main and b/Backend/main differ diff --git a/README.md b/README.md index a044cf0..2fff85b 100644 --- a/README.md +++ b/README.md @@ -11,3 +11,7 @@ Encrypted messaging app - Add message pagination - Finish off conversation settings page - Finish message expiry +- Add back button to QR scanner +- Add more padding at message send text box +- Fix error when creating existing conversation between friends +- Sort conversation based on latest message diff --git a/mobile/lib/models/conversations.dart b/mobile/lib/models/conversations.dart index 621dfae..c07683f 100644 --- a/mobile/lib/models/conversations.dart +++ b/mobile/lib/models/conversations.dart @@ -37,6 +37,7 @@ Future createConversation(String title, List friends, bool twoUser: twoUser, status: ConversationStatus.pending, isRead: true, + messageExpiryDefault: 'no_expiry' ); await db.insert( @@ -161,6 +162,7 @@ Future getConversationById(String id) async { status: ConversationStatus.values[maps[0]['status']], isRead: maps[0]['is_read'] == 1, icon: file, + messageExpiryDefault: maps[0]['message_expiry'], ); } @@ -190,6 +192,7 @@ Future> getConversations() async { status: ConversationStatus.values[maps[i]['status']], isRead: maps[i]['is_read'] == 1, icon: file, + messageExpiryDefault: maps[i]['message_expiry'] ?? 'no_expiry', ); }); } @@ -223,6 +226,7 @@ Future getTwoUserConversation(String userId) async { twoUser: maps[0]['two_user'] == 1, status: ConversationStatus.values[maps[0]['status']], isRead: maps[0]['is_read'] == 1, + messageExpiryDefault: maps[0]['message_expiry'], ); } @@ -236,6 +240,7 @@ class Conversation { bool twoUser; ConversationStatus status; bool isRead; + String messageExpiryDefault = 'no_expiry'; File? icon; Conversation({ @@ -247,6 +252,7 @@ class Conversation { required this.twoUser, required this.status, required this.isRead, + required this.messageExpiryDefault, this.icon, }); @@ -276,6 +282,7 @@ class Conversation { twoUser: false, status: ConversationStatus.complete, isRead: true, + messageExpiryDefault: 'no_expiry', ); } @@ -321,6 +328,7 @@ class Conversation { 'name': AesHelper.aesEncrypt(symKey, Uint8List.fromList(name.codeUnits)), 'users': await getEncryptedConversationUsers(this, symKey), 'two_user': AesHelper.aesEncrypt(symKey, Uint8List.fromList((twoUser ? 'true' : 'false').codeUnits)), + 'message_expiry': messageExpiryDefault, 'user_conversations': userConversations, }; @@ -358,6 +366,7 @@ class Conversation { 'status': status.index, 'is_read': isRead ? 1 : 0, 'file': icon != null ? icon!.path : null, + 'message_expiry': messageExpiryDefault, }; } diff --git a/mobile/lib/models/my_profile.dart b/mobile/lib/models/my_profile.dart index a5d2f66..cbf97ed 100644 --- a/mobile/lib/models/my_profile.dart +++ b/mobile/lib/models/my_profile.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:Capsule/utils/storage/get_file.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:pointycastle/impl.dart'; import 'package:shared_preferences/shared_preferences.dart'; diff --git a/mobile/lib/utils/storage/conversations.dart b/mobile/lib/utils/storage/conversations.dart index 9d1367f..198d26b 100644 --- a/mobile/lib/utils/storage/conversations.dart +++ b/mobile/lib/utils/storage/conversations.dart @@ -116,6 +116,8 @@ Future updateConversations() async { var conversationDetailJson = conversationsDetailsJson[i] as Map; var conversation = findConversationByDetailId(conversations, conversationDetailJson['id']); + conversation.messageExpiryDefault = conversationDetailJson['message_expiry']; + conversation.twoUser = AesHelper.aesDecrypt( base64.decode(conversation.symmetricKey), base64.decode(conversationDetailJson['two_user']), @@ -194,7 +196,7 @@ Future uploadConversation(Conversation conversation, BuildContext context) body: jsonEncode(conversationJson), ); - if (resp.statusCode != 200) { + if (resp.statusCode != 204) { showMessage('Failed to create conversation', context); } } diff --git a/mobile/lib/utils/storage/database.dart b/mobile/lib/utils/storage/database.dart index 0d85531..7670074 100644 --- a/mobile/lib/utils/storage/database.dart +++ b/mobile/lib/utils/storage/database.dart @@ -40,7 +40,8 @@ Future getDatabaseConnection() async { two_user INTEGER, status INTEGER, is_read INTEGER, - file TEXT + file TEXT, + message_expiry TEXT ); '''); diff --git a/mobile/lib/views/main/conversation/detail.dart b/mobile/lib/views/main/conversation/detail.dart index ddc3e58..61658ff 100644 --- a/mobile/lib/views/main/conversation/detail.dart +++ b/mobile/lib/views/main/conversation/detail.dart @@ -271,8 +271,14 @@ class _ConversationDetailState extends State { ], ), - showFilePicker ? + AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + transitionBuilder: (Widget child, Animation animation) { + return SizeTransition(sizeFactor: animation, child: child); + }, + child: showFilePicker ? FilePicker( + key: const Key('filePicker'), cameraHandle: (XFile image) {}, galleryHandleMultiple: (List images) async { for (var img in images) { @@ -284,7 +290,8 @@ class _ConversationDetailState extends State { }, fileHandle: () {}, ) : - const SizedBox.shrink(), + const SizedBox(height: 15), + ), ], ), ), diff --git a/mobile/lib/views/main/conversation/settings.dart b/mobile/lib/views/main/conversation/settings.dart index e68d88c..9aacc23 100644 --- a/mobile/lib/views/main/conversation/settings.dart +++ b/mobile/lib/views/main/conversation/settings.dart @@ -1,14 +1,18 @@ +import 'dart:convert'; import 'dart:io'; -import 'package:Capsule/components/custom_title_bar.dart'; -import 'package:Capsule/components/flash_message.dart'; -import 'package:Capsule/exceptions/update_data_exception.dart'; -import 'package:Capsule/models/friends.dart'; -import 'package:Capsule/utils/encryption/crypto_utils.dart'; -import 'package:Capsule/utils/storage/write_file.dart'; -import 'package:Capsule/views/main/conversation/create_add_users.dart'; +import 'package:Capsule/utils/storage/session_cookie.dart'; import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import '/components/custom_title_bar.dart'; +import '/components/flash_message.dart'; +import '/components/select_message_ttl.dart'; +import '/exceptions/update_data_exception.dart'; +import '/models/friends.dart'; +import '/utils/encryption/crypto_utils.dart'; +import '/utils/storage/write_file.dart'; +import '/views/main/conversation/create_add_users.dart'; import '/models/conversation_users.dart'; import '/models/conversations.dart'; import '/models/my_profile.dart'; @@ -122,13 +126,7 @@ class _ConversationSettingsState extends State { widget.conversation.name = conversationName; widget.conversation.icon = writtenFile; - final db = await getDatabaseConnection(); - db.update( - 'conversations', - widget.conversation.toMap(), - where: 'id = ?', - whereArgs: [widget.conversation.id], - ); + await saveConversation(); await updateConversation(widget.conversation, updatedImage: updatedImage) .catchError((error) { @@ -256,7 +254,38 @@ class _ConversationSettingsState extends State { ) ), onPressed: () { - print('Disappearing Messages'); + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => SelectMessageTTL( + widgetTitle: 'Message Expiry', + currentSelected: widget.conversation.messageExpiryDefault, + backCallback: (String messageExpiry) async { + widget.conversation.messageExpiryDefault = messageExpiry; + + http.post( + await MyProfile.getServerUrl( + 'api/v1/auth/conversations/${widget.conversation.id}/message_expiry' + ), + headers: { + 'cookie': await getSessionCookie(), + }, + body: jsonEncode({ + 'message_expiry': messageExpiry, + }), + ).then((http.Response response) { + if (response.statusCode == 204) { + return; + } + + showMessage( + 'Could not change the default message expiry, please try again later.', + context, + ); + }); + + saveConversation(); + } + )) + ); } ), TextButton.icon( @@ -331,5 +360,15 @@ return Theme.of(context).colorScheme.onBackground; getUsers(); setState(() {}); } + + saveConversation() async { + final db = await getDatabaseConnection(); + db.update( + 'conversations', + widget.conversation.toMap(), + where: 'id = ?', + whereArgs: [widget.conversation.id], + ); + } } diff --git a/mobile/lib/views/main/profile/profile.dart b/mobile/lib/views/main/profile/profile.dart index 615b232..63bd867 100644 --- a/mobile/lib/views/main/profile/profile.dart +++ b/mobile/lib/views/main/profile/profile.dart @@ -64,7 +64,6 @@ class _ProfileState extends State { backdropEnabled: true, backdropOpacity: 0.2, minHeight: 0, - maxHeight: 450, panel: Center( child: _profileQrCode(), ), @@ -73,7 +72,13 @@ class _ProfileState extends State { child: Column( children: [ usernameHeading(), - fileSelector(), + AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + transitionBuilder: (Widget child, Animation animation) { + return SizeTransition(sizeFactor: animation, child: child); + }, + child: fileSelector(), + ), SizedBox(height: showFileSelector ? 10 : 30), settings(), const SizedBox(height: 30), @@ -126,7 +131,8 @@ class _ProfileState extends State { return const SizedBox.shrink(); } - return Padding( + return Padding( + key: const Key('fileSelector'), padding: const EdgeInsets.only(top: 10), child: FilePicker( cameraHandle: _setProfileImage,