Added more unit tests and resolve a minor edge case

Added some more unit tests
Added logic to insert a fallback "invite" control message if a group is created via the USER_GROUPS config message instead of an invite
pull/941/head
Morgan Pretty 1 year ago
parent d16cbcaeb6
commit 42f4925c36

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

File diff suppressed because it is too large Load Diff

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -393,12 +393,12 @@
"MESSAGE_REQUESTS_TITLE" = "Žádosti o zprávy";
"MESSAGE_REQUESTS_EMPTY_TEXT" = "Žádné nevyřízené žádosti o zprávu";
"MESSAGE_REQUESTS_CLEAR_ALL" = "Smazat vše";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests and group invites?";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Jste si jisti, že chcete vymazat všechny žádosti o zprávy a pozvánky do skupin?";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "Smazat";
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "Jste si jisti, že chcete odstranit tuto žádost o zprávu?";
"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "Opravdu chcete zablokovat tento kontakt?";
"MESSAGE_REQUESTS_INFO" = "Odesláním zprávy tomuto uživateli automaticky přijmete jejich požadavek na zprávu a odhalíte jim své Session ID.";
"MESSAGE_REQUESTS_ACCEPTED" = "Your message request has been accepted.";
"MESSAGE_REQUESTS_INFO" = "Odesláním zprávy tomuto uživateli automaticky přijmete jejich žádost o zprávu a odhalíte jim své Session ID.";
"MESSAGE_REQUESTS_ACCEPTED" = "Vaše žádost o zprávu byla přijata.";
"MESSAGE_REQUESTS_NOTIFICATION" = "Máte novou žádost o zprávu";
"TXT_HIDE_TITLE" = "Skrýt";
"TXT_DELETE_ACCEPT" = "Přijmout";
@ -602,7 +602,7 @@
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Žádosti o zprávy";
/* PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE */
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Požadavky na zprávy komunity";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Žádosti o zrávy z komunit";
/* PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION */
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Povolit požadavky na zprávy z konverzací komunity.";
@ -873,10 +873,10 @@ The point that a message will disappear in a disappearing message update message
"GROUP_MESSAGE_INFO_REMOVED" = "Byli jste odebráni %@.";
/* Description of a warning prompt when deleting an invitation to join a group conversation. */
"MESSAGE_REQUESTS_GROUP_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this group invite?";
"MESSAGE_REQUESTS_GROUP_DELETE_CONFIRMATION_ACTON" = "Jste si jisti, že chcete smazat tuto pozvánku do skupiny?";
/* Description of a confirmation prompt when blocking an invitation to join a group conversation. The '%@' will be replaced with the name of the user that sent the invitation. */
"MESSAGE_REQUESTS_GROUP_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block %@? Blocked users cannot send you message requests, group invites or call you.";
"MESSAGE_REQUESTS_GROUP_BLOCK_CONFIRMATION_ACTON" = "Jste si jisti, že chcete zablokovat %@? Zablokovaní uživatelé vám nemohou posílat žádosti o zprávy, pozvánky do skupin ani vám nemohou volat.";
/* An informational message displayed when the user has been invited to join a group, the first '%@' will be the name of the user that sent the invitation and the second '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED" = "%@ vás pozval(a) přidat se k %@.";
@ -885,7 +885,7 @@ The point that a message will disappear in a disappearing message update message
"group_unable_to_delete" = "Nelze smazat skupinu, zkuste to prosím znovu.";
/* Information displayed above the input when opening an invitation to join a group. */
"GROUP_MESSAGE_REQUEST_INFO" = "Sending a message to this group will automatically accept the group invite.";
"GROUP_MESSAGE_REQUEST_INFO" = "Odesláním zprávy do této skupiny automaticky přijmete pozvánku do skupiny.";
/* An error indicating we were unable to retrieve the required data for some reason. */
"ERROR_UNABLE_TO_FIND_DATA" = "Došlo k problému při načítání požadovaných dat. Zkuste to prosím později.";
@ -978,25 +978,28 @@ The point that a message will disappear in a disappearing message update message
"GROUP_ACTION_PROMOTE_SENDING_MULTIPLE" = "Odesílání propagací";
/* A toast which indicates that a single promotion to admin within a group failed to send, the first '%@' will be the name of the member that couldn't be promoted and the second '%@' will be the name of the group. */
"GROUP_ACTION_PROMOTE_FAILED_ONE" = "Failed to promote %@ in %@";
"GROUP_ACTION_PROMOTE_FAILED_ONE" = "Selhalo povýšení %@ v %@";
/* A toast which indicates that two promotions to admin within a group failed to send, the first '%@' will be the name of the first member that couldn't be promoted, the second '%@' will be the name of the second member that couldn't be promoted, and the third '%@' will be the name of the group. */
"GROUP_ACTION_PROMOTE_FAILED_TWO" = "Failed to promote %@ and %@ in %@";
"GROUP_ACTION_PROMOTE_FAILED_TWO" = "Selhalo povýšení %@ a %@ v %@";
/* A toast which indicates multiple promotions to admin within a group failed to send, the first '%@' will be the name of the first member that couldn't be promoted, the second '%@' will be the number of other members that couldn't be promoted, and the third '%@' will be the name of the group. */
"GROUP_ACTION_PROMOTE_FAILED_MULTIPLE" = "Failed to promote %@ and %@ others in %@";
"GROUP_ACTION_PROMOTE_FAILED_MULTIPLE" = "Selhalo povýšení %@ a %@ dalších v %@";
/* A warning shown at the top of a conversation to indicate that the conversation is a legacy group conversation which will stop functioning correctly on a certain date, the '%@' will be replaced with the date it will stop working. */
"LEGACY_GROUPS_DEPRECATED_BANNER" = "Groups have been upgraded, create a new group to upgrade. Old group functionality will be degraded from %@.";
"LEGACY_GROUPS_DEPRECATED_BANNER" = "Skupiny byly přepracovány, pro využití tohoto upgrade vytvořte prosím novou skupinu. Funkčnost staré skupiny bude omezena od %@.";
/* Title for the prompt which appears when editing the group name and description. */
"EDIT_GROUP_INFO_TITLE" = "Update Group Information";
"EDIT_GROUP_INFO_TITLE" = "Upravit informace o skupině";
/* Message for the prompt which appears when editing the group name and description. */
"EDIT_GROUP_INFO_MESSAGE" = "Group name and description is visible to all group members.";
"EDIT_GROUP_INFO_MESSAGE" = "Název a popis skupiny jsou viditelné pro všechny členy skupiny.";
/* Title for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_TITLE" = "Update Group Name";
"EDIT_LEGACY_GROUP_INFO_TITLE" = "Upravit název skupiny";
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Název skupiny je viditelný pro všechny členy skupiny.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1001,3 +1001,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -393,7 +393,7 @@
"MESSAGE_REQUESTS_TITLE" = "消息请求";
"MESSAGE_REQUESTS_EMPTY_TEXT" = "没有待处理的消息请求";
"MESSAGE_REQUESTS_CLEAR_ALL" = "清除所有";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "Are you sure you want to clear all message requests and group invites?";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_TITLE" = "您确定要清除所有消息请求和群组邀请吗?";
"MESSAGE_REQUESTS_CLEAR_ALL_CONFIRMATION_ACTON" = "清除";
"MESSAGE_REQUESTS_DELETE_CONFIRMATION_ACTON" = "您确定要删除此消息请求吗?";
"MESSAGE_REQUESTS_BLOCK_CONFIRMATION_ACTON" = "您确定要屏蔽该联系人吗?";
@ -578,7 +578,7 @@
"DISAPPERING_MESSAGES_INFO_DISABLE" = "%@ 关闭了阅后即焚消息";
/* context_menu_info */
"context_menu_info" = "Info";
"context_menu_info" = "信息";
/* An error that is displayed when the application fails for create it's initial connection to the database */
"DATABASE_STARTUP_FAILED" = "An error occurred when opening the database\n\nYou can export your application logs to share for troubleshooting or you can try to restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
@ -587,58 +587,58 @@
"APP_STARTUP_TIMEOUT" = "The app is taking a long time to start\n\nYou can continue to wait for the app to start, export your application logs to share for troubleshooting or you can try to open the app again";
/* The title of a button on a modal shown when the application fails to start, pressing the button closes the application */
"APP_STARTUP_EXIT" = "Exit";
"APP_STARTUP_EXIT" = "退出";
/* An error which occurs if the user tries to restore the database after an initial failure and it fails to restore */
"DATABASE_RESTORE_FAILED" = "An error occurred when opening the restored database\n\nYou can export your application logs to share for troubleshooting but to continue to use Session you may need to reinstall";
/* Text displayed in place of a quoted message when the original message is not on the device */
"QUOTED_MESSAGE_NOT_FOUND" = "Original message not found.";
"QUOTED_MESSAGE_NOT_FOUND" = "未找到原消息";
/* EMOJI_REACTS_SHOW_LESS */
"EMOJI_REACTS_SHOW_LESS" = "Show less";
"EMOJI_REACTS_SHOW_LESS" = "隐藏细节";
/* PRIVACY_SECTION_MESSAGE_REQUESTS */
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "Message Requests";
"PRIVACY_SECTION_MESSAGE_REQUESTS" = "消息请求";
/* PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE */
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "Community Message Requests";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_TITLE" = "群组消息请求";
/* PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION */
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "Allow message requests from Community conversations.";
"PRIVACY_SCREEN_MESSAGE_REQUESTS_COMMUNITY_DESCRIPTION" = "允许来自群组会话内成员的消息请求";
/* Information displayed above the input when sending a message to a new user for the first time explaining limitations around the types of messages which can be sent before being approved */
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "对方同意消息请求后,您将可以发送语音信息及附件";
/* State of a message while it's still in the process of being sent */
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
"MESSAGE_DELIVERY_STATUS_SENDING" = "发送中";
/* State of a message once it has been sent */
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_SENT" = "已发送";
/* State of a message after the recipient has read the message */
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_READ" = "已读";
/* State of a message if it failed to be sent */
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
"MESSAGE_DELIVERY_STATUS_FAILED" = "发送失败";
/* Title of the message information screen describing the date/time a message was sent */
"MESSAGE_INFO_SENT" = "Sent";
"MESSAGE_INFO_SENT" = "已发送";
/* Title of the message information screen describing the date/time a message was received on a specific device */
"MESSAGE_INFO_RECEIVED" = "Received";
"MESSAGE_INFO_RECEIVED" = "已接收";
/* Title of the message information screen describing the sender of the message */
"MESSAGE_INFO_FROM" = "From";
"MESSAGE_INFO_FROM" = "来自";
/* Title of the message information screen describing the identifier of the attachment */
"ATTACHMENT_INFO_FILE_ID" = "File ID";
"ATTACHMENT_INFO_FILE_ID" = "文件ID";
/* Title of the message information screen describing the file type of the attachment */
"ATTACHMENT_INFO_FILE_TYPE" = "File Type";
"ATTACHMENT_INFO_FILE_TYPE" = "文件类型";
/* Title of the message information screen describing the size of the attachment */
"ATTACHMENT_INFO_FILE_SIZE" = "File Size";
"ATTACHMENT_INFO_FILE_SIZE" = "文件大小";
/* Title on the message information screen describing the resolution of a media attachment */
"ATTACHMENT_INFO_RESOLUTION" = "Resolution";
@ -647,88 +647,88 @@
"ATTACHMENT_INFO_DURATION" = "Duration";
/* State of a message after it failed to sync to the current users other devices */
"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync";
"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "同步失败";
/* State of a message while it's in the process of being synced to the users other devices */
"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing";
"MESSAGE_DELIVERY_STATUS_SYNCING" = "正在同步";
/* Title of the modal that appears after a user taps on the state of a message which failed to send */
"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message";
"MESSAGE_DELIVERY_FAILED_TITLE" = "消息发送失败";
/* Title of the modal that appears after a user taps on the state of a message which failed to sync to the users other devices */
"MESSAGE_DELIVERY_FAILED_SYNC_TITLE" = "Failed to sync message to your other devices";
"MESSAGE_DELIVERY_FAILED_SYNC_TITLE" = "未能将消息同步至其他设备";
/* Action for the modal shown when asking the user whether they want to delete from all of their devices */
"delete_message_for_me_and_my_devices" = "Delete from all of my devices";
"delete_message_for_me_and_my_devices" = "从我的所有设备删除";
/* Action in the long-press menu to trigger a message to be sent again after it has failed */
"context_menu_resend" = "Resend";
"context_menu_resend" = "重新发送";
/* Action in the long-press menu to trigger a message to be synced again after it has failed */
"context_menu_resync" = "Resync";
"context_menu_resync" = "重新同步";
/* Title of a modal show the first time a user tries to search for GIFs */
"GIPHY_PERMISSION_TITLE" = "Search GIFs?";
"GIPHY_PERMISSION_TITLE" = "在查找GIF动态图片吗";
/* Message of a modal show the first time a user tries to search for GIFs */
"GIPHY_PERMISSION_MESSAGE" = "Session will connect to Giphy to provide search results. You will not have full metadata protection when sending GIFs.";
"GIPHY_PERMISSION_MESSAGE" = "Session将会连接到Giphy以提供搜索结果。当您发送GIF动图时您的元数据无法受到完整保护。";
/* Action in the long-press menu to view more information about a specific message */
"message_info_title" = "Message Info";
"message_info_title" = "会话信息";
/* Action to mute a conversation in the swipe menu */
"mute_button_text" = "Mute";
"mute_button_text" = "静音";
/* Action in the swipe menu to unmute a conversation */
"unmute_button_text" = "Unmute";
"unmute_button_text" = "解除静音";
/* Action in the swipe menu to mark a conversation as read */
"MARK_AS_READ" = "Mark read";
"MARK_AS_READ" = "标记为已读";
/* Action in the swipe menu to mark a conversation as unread */
"MARK_AS_UNREAD" = "Mark unread";
"MARK_AS_UNREAD" = "标记为未读";
/* Title of the confirmation modal show when attempting to leave a group conversation */
"leave_group_confirmation_alert_title" = "Leave Group";
"leave_group_confirmation_alert_title" = "离开群组";
/* Title of the confirmation modal show when attempting to leave a community conversation */
"leave_community_confirmation_alert_title" = "Leave Community";
"leave_community_confirmation_alert_title" = "离开社区";
/* Message in the confirmation modal when leaving a community conversation */
"leave_community_confirmation_alert_message" = "Are you sure you want to leave %@?";
"leave_community_confirmation_alert_message" = "确定要退出%@吗?";
/* Conversation subtitle while the user in the process of leaving */
"group_you_leaving" = "Leaving...";
"group_you_leaving" = "退出中...";
/* Conversation subtitle if the user in the failed to leave */
"group_leave_error" = "Failed to leave Group!";
"group_leave_error" = "退出群组失败!";
/* Message within a conversation indicating the device was unable to leave a group conversation */
"group_unable_to_leave" = "Unable to leave the Group, please try again";
"group_unable_to_leave" = "未能退出群组,请重试";
/* Title in the confirmation modal to delete a group */
"delete_group_confirmation_alert_title" = "Delete Group";
"delete_group_confirmation_alert_title" = "删除群组";
/* Message in the confirmation modal to delete a group */
"delete_group_confirmation_alert_message" = "Are you sure you want to delete %@?";
"delete_group_confirmation_alert_message" = "您确定要删除%@吗?";
/* Title in the confirmation modal when the user tries to delete a one-to-one conversation */
"delete_conversation_confirmation_alert_title" = "Delete Conversation";
"delete_conversation_confirmation_alert_title" = "删除会话";
/* Message in the confirmation modal when the user tries to delete a one-to-one conversation */
"delete_conversation_confirmation_alert_message" = "Are you sure you want to delete your conversation with %@?";
/* Title in the confirmation modal when the user tries to hide the 'Note to Self' conversation */
"hide_note_to_self_confirmation_alert_title" = "Hide Note to Self";
"hide_note_to_self_confirmation_alert_title" = "隐藏备忘录";
/* Message in the confirmation modal when the user tries to hide the 'Note to Self' conversation */
"hide_note_to_self_confirmation_alert_message" = "Are you sure you want to hide %@?";
"hide_note_to_self_confirmation_alert_message" = "您确定要隐藏%@吗?";
/* Title in the modal for updating the users profile display picture */
"update_profile_modal_title" = "Set Display Picture";
"update_profile_modal_title" = " 设置展示图片";
/* Save action in the modal for updating the users profile display picture */
"update_profile_modal_save" = "Save";
"update_profile_modal_save" = "保存";
/* Remove action in the modal for updating the users profile display picture */
"update_profile_modal_remove" = "Remove";
@ -864,43 +864,43 @@ The point that a message will disappear in a disappearing message update message
"GROUP_MESSAGE_INFO_MEMBER_PROMOTED" = "%@ was promoted to Admin.";
/* An informational message displayed when two members of the group were promoted to admin, the '%@' will be the members names. */
"GROUP_MESSAGE_INFO_TWO_MEMBERS_PROMOTED" = "%@ and %@ were promoted to Admin.";
"GROUP_MESSAGE_INFO_TWO_MEMBERS_PROMOTED" = "%@和%@被设置为管理员";
/* An informational message displayed when multiple members of the group were promoted to admin, the first '%@' will be the first members name and the second '%@' will be the number of additional members promoted. */
"GROUP_MESSAGE_INFO_MULTIPLE_MEMBERS_PROMOTED" = "%@ and %@ others were promoted to Admin.";
/* An informational message displayed the current user was removed from a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_REMOVED" = "You were removed from %@.";
"GROUP_MESSAGE_INFO_REMOVED" = "您被从%@中移除";
/* Description of a warning prompt when deleting an invitation to join a group conversation. */
"MESSAGE_REQUESTS_GROUP_DELETE_CONFIRMATION_ACTON" = "Are you sure you want to delete this group invite?";
/* Description of a confirmation prompt when blocking an invitation to join a group conversation. The '%@' will be replaced with the name of the user that sent the invitation. */
"MESSAGE_REQUESTS_GROUP_BLOCK_CONFIRMATION_ACTON" = "Are you sure you want to block %@? Blocked users cannot send you message requests, group invites or call you.";
"MESSAGE_REQUESTS_GROUP_BLOCK_CONFIRMATION_ACTON" = "您确定要屏蔽%@吗?被屏蔽的用户将无法向您发送消息请求、群聊邀请或者语音通话。";
/* An informational message displayed when the user has been invited to join a group, the first '%@' will be the name of the user that sent the invitation and the second '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED" = "%@ invited you to join %@.";
"GROUP_MESSAGE_INFO_INVITED" = "%@邀请您加入%@";
/* Message within a conversation indicating the device was unable to delete a group conversation */
"group_unable_to_delete" = "Unable to delete the Group, please try again.";
"group_unable_to_delete" = "未能删除群组,请重试";
/* Information displayed above the input when opening an invitation to join a group. */
"GROUP_MESSAGE_REQUEST_INFO" = "Sending a message to this group will automatically accept the group invite.";
"GROUP_MESSAGE_REQUEST_INFO" = "向此群组发送消息将会自动接收群聊邀请";
/* An error indicating we were unable to retrieve the required data for some reason. */
"ERROR_UNABLE_TO_FIND_DATA" = "There is an issue retrieving the required data. Please try again later.";
/* A title for the list of group members. */
"GROUP_MEMBERS" = "Group Members";
"GROUP_MEMBERS" = "群成员";
/* The status for a group member while their invite is being sent. */
"GROUP_MEMBER_STATUS_SENDING" = "Sending invite";
"GROUP_MEMBER_STATUS_SENDING" = "发送邀请中";
/* The status for a group member while their invite is pending. */
"GROUP_MEMBER_STATUS_SENT" = "Invite sent";
"GROUP_MEMBER_STATUS_SENT" = "邀请已发送";
/* The status for a group member if their invitation failed to send. */
"GROUP_MEMBER_STATUS_FAILED" = "Invite failed";
"GROUP_MEMBER_STATUS_FAILED" = "邀请失败";
/* The status for a group admin while their invite is being sent. */
"GROUP_ADMIN_STATUS_SENDING" = "Sending admin promotion";
@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -1000,3 +1000,6 @@ The point that a message will disappear in a disappearing message update message
/* Message for the prompt which appears when editing a legacy group name. */
"EDIT_LEGACY_GROUP_INFO_MESSAGE" = "Group name is visible to all group members.";
/* An informational message displayed when the user has been invited to join a group, the '%@' will be the name of the group. */
"GROUP_MESSAGE_INFO_INVITED_FALLBACK" = "You were invited to join %@.";

@ -433,6 +433,7 @@ public extension ClosedGroup {
public extension ClosedGroup {
enum MessageInfo: Codable {
case invited(String, String)
case invitedFallback(String)
case updatedName(String)
case updatedNameFallback
case updatedDisplayPicture
@ -451,6 +452,12 @@ public extension ClosedGroup {
.font(groupName, .boldSystemFont(ofSize: Values.verySmallFontSize))
)
case .invitedFallback(let groupName):
return NSAttributedString(
format: "GROUP_MESSAGE_INFO_INVITED_FALLBACK".localized(),
.font(groupName, .boldSystemFont(ofSize: Values.verySmallFontSize))
)
case .updatedName(let name):
return NSAttributedString(
format: "GROUP_MESSAGE_INFO_NAME_UPDATED_TO".localized(),

@ -160,6 +160,13 @@ extension MessageReceiver {
/// an 'invited' info message
guard !threadAlreadyExisted || wasKickedFromGroup else { return }
/// Remove any existing `infoGroupInfoInvited` interactions from the group (don't want to have a duplicate one in case
/// the group was created via a `USER_GROUPS` config when syncing a new device)
_ = try Interaction
.filter(Interaction.Columns.threadId == message.groupSessionId.hexString)
.filter(Interaction.Columns.variant == Interaction.Variant.infoGroupInfoInvited)
.deleteAll(db)
let interaction: Interaction = try Interaction(
threadId: message.groupSessionId.hexString,
authorId: sender,

@ -370,7 +370,7 @@ private struct MemberData {
// MARK: - Convenience
private extension SessionUtil {
internal extension SessionUtil {
static func extractMembers(
from conf: UnsafeMutablePointer<config_object>?,
groupSessionId: SessionId

@ -153,7 +153,8 @@ internal extension SessionUtil {
),
priority: group.priority,
joinedAt: TimeInterval(group.joined_at),
invited: group.invited
invited: group.invited,
wasKickedFromGroup: ugroups_group_is_kicked(&group)
)
)
}
@ -430,6 +431,7 @@ internal extension SessionUtil {
// MARK: -- Handle Group Changes
let userSessionId: SessionId = getUserSessionId(db, using: dependencies)
let existingGroupSessionIds: Set<String> = Set(existingThreadInfo
.filter { $0.value.variant == .group }
.keys)
@ -454,6 +456,28 @@ internal extension SessionUtil {
using: dependencies
)
/// If the thread didn't already exist, or the user had previously been kicked but has since been re-added to the group, then insert
/// a fallback 'invited' info message
if existingGroups[group.groupSessionId] == nil || group.wasKickedFromGroup == true {
_ = try Interaction(
threadId: group.groupSessionId,
authorId: group.groupSessionId,
variant: .infoGroupInfoInvited,
body: ClosedGroup.MessageInfo
.invitedFallback(group.name ?? "GROUP_TITLE_FALLBACK".localized())
.infoString(using: dependencies),
timestampMs: (group.joinedAt.map { Int64(Double($0 * 1000)) } ?? serverTimestampMs),
wasRead: SessionUtil.timestampAlreadyRead(
threadId: group.groupSessionId,
threadVariant: .group,
timestampMs: (group.joinedAt.map { Int64(Double($0 * 1000)) } ?? serverTimestampMs),
userSessionId: userSessionId,
openGroup: nil,
using: dependencies
)
).inserted(db)
}
case (.some(let existingGroup), _):
let joinedAt: TimeInterval = (
group.joinedAt.map { TimeInterval($0) } ??
@ -1292,6 +1316,7 @@ extension SessionUtil {
let priority: Int32?
let joinedAt: TimeInterval?
let invited: Bool?
let wasKickedFromGroup: Bool?
init(
groupSessionId: String,
@ -1300,7 +1325,8 @@ extension SessionUtil {
authData: Data? = nil,
priority: Int32? = nil,
joinedAt: TimeInterval? = nil,
invited: Bool? = nil
invited: Bool? = nil,
wasKickedFromGroup: Bool? = nil
) {
self.groupSessionId = groupSessionId
self.groupIdentityPrivateKey = groupIdentityPrivateKey
@ -1309,6 +1335,7 @@ extension SessionUtil {
self.priority = priority
self.joinedAt = joinedAt
self.invited = invited
self.wasKickedFromGroup = wasKickedFromGroup
}
}
}

@ -16,6 +16,8 @@ class DisplayPictureDownloadJobSpec: QuickSpec {
@TestState var job: Job!
@TestState var profile: Profile!
@TestState var group: ClosedGroup!
@TestState var community: OpenGroup!
@TestState var dependencies: TestDependencies! = TestDependencies { dependencies in
dependencies.forceSynchronous = true
dependencies.dateNow = Date(timeIntervalSince1970: 1234567890)
@ -718,6 +720,18 @@ class DisplayPictureDownloadJobSpec: QuickSpec {
_ = try Profile.deleteAll(db)
try profile.insert(db)
}
job = Job(
variant: .displayPictureDownload,
shouldBeUnique: true,
details: DisplayPictureDownloadJob.Details(
target: .profile(
id: "1234",
url: "http://oxen.io/100/",
encryptionKey: encryptionKey
),
timestamp: 1234567891
)
)
}
// MARK: ------ that does not exist
@ -766,7 +780,7 @@ class DisplayPictureDownloadJobSpec: QuickSpec {
Profile(
id: "1234",
name: "test",
profilePictureUrl: "test",
profilePictureUrl: "http://oxen.io/100/",
profilePictureFileName: "\(filenameUuid.uuidString).jpg",
profileEncryptionKey: encryptionKey,
lastProfilePictureUpdate: 1234567891
@ -802,7 +816,7 @@ class DisplayPictureDownloadJobSpec: QuickSpec {
Profile(
id: "1234",
name: "test",
profilePictureUrl: "test",
profilePictureUrl: "http://oxen.io/100/",
profilePictureFileName: "\(filenameUuid.uuidString).jpg",
profileEncryptionKey: encryptionKey,
lastProfilePictureUpdate: 1234567891
@ -868,6 +882,380 @@ class DisplayPictureDownloadJobSpec: QuickSpec {
))
}
}
// MARK: ---- for a group
context("for a group") {
beforeEach {
group = ClosedGroup(
threadId: "03cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece",
name: "TestGroup",
groupDescription: nil,
formationTimestamp: 1234567890,
displayPictureUrl: "http://oxen.io/100/",
displayPictureFilename: nil,
displayPictureEncryptionKey: encryptionKey,
lastDisplayPictureUpdate: 1234567890,
shouldPoll: true,
groupIdentityPrivateKey: nil,
authData: Data([1, 2, 3]),
invited: false
)
mockStorage.write(using: dependencies) { db in
_ = try ClosedGroup.deleteAll(db)
try SessionThread.fetchOrCreate(
db,
id: "03cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece",
variant: .group,
shouldBeVisible: true,
calledFromConfigHandling: false
).upsert(db)
try group.insert(db)
}
job = Job(
variant: .displayPictureDownload,
shouldBeUnique: true,
details: DisplayPictureDownloadJob.Details(
target: .group(
id: "03cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece",
url: "http://oxen.io/100/",
encryptionKey: encryptionKey
),
timestamp: 1234567891
)
)
}
// MARK: ------ that does not exist
context("that does not exist") {
beforeEach {
mockStorage.write(using: dependencies) { db in try ClosedGroup.deleteAll(db) }
}
// MARK: -------- does not save the picture
it("does not save the picture") {
expect(mockCrypto)
.toNot(call {
$0.generate(.decryptedDataDisplayPicture(data: .any, key: .any, using: .any))
})
expect(mockFileManager)
.toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) })
expect(mockDisplayPictureCache).toNot(call { $0.imageData = .any })
expect(mockStorage.read { db in try ClosedGroup.fetchOne(db) }).to(beNil())
}
}
// MARK: ------ that has a different encryption key and more recent update
context("that has a different encryption key and more recent update") {
beforeEach {
mockStorage.write(using: dependencies) { db in
try ClosedGroup
.updateAll(
db,
ClosedGroup.Columns.displayPictureEncryptionKey.set(to: Data([1, 2, 3])),
ClosedGroup.Columns.lastDisplayPictureUpdate.set(to: 9999999999)
)
}
}
// MARK: -------- does not save the picture
it("does not save the picture") {
expect(mockCrypto)
.toNot(call {
$0.generate(.decryptedDataDisplayPicture(data: .any, key: .any, using: .any))
})
expect(mockFileManager)
.toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) })
expect(mockDisplayPictureCache).toNot(call { $0.imageData = .any })
expect(mockStorage.read { db in try ClosedGroup.fetchOne(db) })
.toNot(equal(
ClosedGroup(
threadId: "03cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece",
name: "TestGroup",
groupDescription: nil,
formationTimestamp: 1234567890,
displayPictureUrl: "http://oxen.io/100/",
displayPictureFilename: "\(filenameUuid.uuidString).jpg",
displayPictureEncryptionKey: encryptionKey,
lastDisplayPictureUpdate: 1234567891,
shouldPoll: true,
groupIdentityPrivateKey: nil,
authData: Data([1, 2, 3]),
invited: false
)
))
}
}
// MARK: ------ that has a different url and more recent update
context("that has a different url and more recent update") {
beforeEach {
mockStorage.write(using: dependencies) { db in
try ClosedGroup
.updateAll(
db,
ClosedGroup.Columns.displayPictureUrl.set(to: "testUrl"),
ClosedGroup.Columns.lastDisplayPictureUpdate.set(to: 9999999999)
)
}
}
// MARK: -------- does not save the picture
it("does not save the picture") {
expect(mockCrypto)
.toNot(call {
$0.generate(.decryptedDataDisplayPicture(data: .any, key: .any, using: .any))
})
expect(mockFileManager)
.toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) })
expect(mockDisplayPictureCache).toNot(call { $0.imageData = .any })
expect(mockStorage.read { db in try ClosedGroup.fetchOne(db) })
.toNot(equal(
ClosedGroup(
threadId: "03cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece",
name: "TestGroup",
groupDescription: nil,
formationTimestamp: 1234567890,
displayPictureUrl: "http://oxen.io/100/",
displayPictureFilename: "\(filenameUuid.uuidString).jpg",
displayPictureEncryptionKey: encryptionKey,
lastDisplayPictureUpdate: 1234567891,
shouldPoll: true,
groupIdentityPrivateKey: nil,
authData: Data([1, 2, 3]),
invited: false
)
))
}
}
// MARK: ------ that has a more recent update but the same url and encryption key
context("that has a more recent update but the same url and encryption key") {
beforeEach {
mockStorage.write(using: dependencies) { db in
try ClosedGroup
.updateAll(
db,
ClosedGroup.Columns.lastDisplayPictureUpdate.set(to: 9999999999)
)
}
}
// MARK: -------- saves the picture
it("saves the picture") {
expect(mockCrypto)
.to(call {
$0.generate(.decryptedDataDisplayPicture(data: .any, key: .any, using: .any))
})
expect(mockFileManager).to(call(.exactly(times: 1), matchingParameters: .all) {
$0.createFile(
atPath: "/test/ProfileAvatars/\(filenameUuid.uuidString).jpg",
contents: imageData,
attributes: nil
)
})
expect(mockDisplayPictureCache).to(call(.exactly(times: 1), matchingParameters: .all) {
$0.imageData = ["\(filenameUuid.uuidString).jpg": imageData]
})
expect(mockStorage.read { db in try ClosedGroup.fetchOne(db) })
.to(equal(
ClosedGroup(
threadId: "03cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece",
name: "TestGroup",
groupDescription: nil,
formationTimestamp: 1234567890,
displayPictureUrl: "http://oxen.io/100/",
displayPictureFilename: "\(filenameUuid.uuidString).jpg",
displayPictureEncryptionKey: encryptionKey,
lastDisplayPictureUpdate: 1234567891,
shouldPoll: true,
groupIdentityPrivateKey: nil,
authData: Data([1, 2, 3]),
invited: false
)
))
}
}
// MARK: ------ updates the database values
it("updates the database values") {
expect(mockStorage.read { db in try ClosedGroup.fetchOne(db) })
.to(equal(
ClosedGroup(
threadId: "03cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece",
name: "TestGroup",
groupDescription: nil,
formationTimestamp: 1234567890,
displayPictureUrl: "http://oxen.io/100/",
displayPictureFilename: "\(filenameUuid.uuidString).jpg",
displayPictureEncryptionKey: encryptionKey,
lastDisplayPictureUpdate: 1234567891,
shouldPoll: true,
groupIdentityPrivateKey: nil,
authData: Data([1, 2, 3]),
invited: false
)
))
}
}
// MARK: ---- for a community
context("for a community") {
beforeEach {
community = OpenGroup(
server: "testServer",
roomToken: "testRoom",
publicKey: "03cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece",
isActive: true,
name: "name",
imageId: "100",
userCount: 1,
infoUpdates: 1,
displayPictureFilename: nil,
lastDisplayPictureUpdate: 1234567890
)
mockStorage.write(using: dependencies) { db in
_ = try OpenGroup.deleteAll(db)
try SessionThread.fetchOrCreate(
db,
id: OpenGroup.idFor(roomToken: "testRoom", server: "testServer"),
variant: .community,
shouldBeVisible: true,
calledFromConfigHandling: false
).upsert(db)
try community.insert(db)
}
job = Job(
variant: .displayPictureDownload,
shouldBeUnique: true,
details: DisplayPictureDownloadJob.Details(
target: .community(
imageId: "100",
roomToken: "testRoom",
server: "testServer"
),
timestamp: 1234567891
)
)
// SOGS doesn't encrypt it's images so replace the encrypted mock response
mockNetwork
.when { $0.send(.selectedNetworkRequest(.any, to: .any, with: .any, timeout: .any, using: .any)) }
.thenReturn(MockNetwork.response(data: imageData))
}
// MARK: ------ that does not exist
context("that does not exist") {
beforeEach {
mockStorage.write(using: dependencies) { db in try OpenGroup.deleteAll(db) }
}
// MARK: -------- does not save the picture
it("does not save the picture") {
expect(mockFileManager)
.toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) })
expect(mockDisplayPictureCache).toNot(call { $0.imageData = .any })
expect(mockStorage.read { db in try OpenGroup.fetchOne(db) }).to(beNil())
}
}
// MARK: ------ that has a different imageId and more recent update
context("that has a different imageId and more recent update") {
beforeEach {
mockStorage.write(using: dependencies) { db in
try OpenGroup
.updateAll(
db,
OpenGroup.Columns.imageId.set(to: "101"),
OpenGroup.Columns.lastDisplayPictureUpdate.set(to: 9999999999)
)
}
}
// MARK: -------- does not save the picture
it("does not save the picture") {
expect(mockFileManager)
.toNot(call { $0.createFile(atPath: .any, contents: .any, attributes: .any) })
expect(mockDisplayPictureCache).toNot(call { $0.imageData = .any })
expect(mockStorage.read { db in try OpenGroup.fetchOne(db) })
.toNot(equal(
OpenGroup(
server: "testServer",
roomToken: "testRoom",
publicKey: "03cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece",
isActive: true,
name: "name",
imageId: "100",
userCount: 1,
infoUpdates: 1,
displayPictureFilename: "\(filenameUuid.uuidString).jpg",
lastDisplayPictureUpdate: 1234567891
)
))
}
}
// MARK: ------ that has a more recent update but the same imageId
context("that has a more recent update but the same imageId") {
beforeEach {
mockStorage.write(using: dependencies) { db in
try OpenGroup
.updateAll(
db,
OpenGroup.Columns.imageId.set(to: "100"),
OpenGroup.Columns.lastDisplayPictureUpdate.set(to: 9999999999)
)
}
}
// MARK: -------- saves the picture
it("saves the picture") {
expect(mockFileManager).to(call(.exactly(times: 1), matchingParameters: .all) {
$0.createFile(
atPath: "/test/ProfileAvatars/\(filenameUuid.uuidString).jpg",
contents: imageData,
attributes: nil
)
})
expect(mockDisplayPictureCache).to(call(.exactly(times: 1), matchingParameters: .all) {
$0.imageData = ["\(filenameUuid.uuidString).jpg": imageData]
})
expect(mockStorage.read { db in try OpenGroup.fetchOne(db) })
.to(equal(
OpenGroup(
server: "testServer",
roomToken: "testRoom",
publicKey: "03cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece",
isActive: true,
name: "name",
imageId: "100",
userCount: 1,
infoUpdates: 1,
displayPictureFilename: "\(filenameUuid.uuidString).jpg",
lastDisplayPictureUpdate: 1234567891
)
))
}
}
// MARK: ------ updates the database values
it("updates the database values") {
expect(mockStorage.read { db in try OpenGroup.fetchOne(db) })
.to(equal(
OpenGroup(
server: "testServer",
roomToken: "testRoom",
publicKey: "03cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece",
isActive: true,
name: "name",
imageId: "100",
userCount: 1,
infoUpdates: 1,
displayPictureFilename: "\(filenameUuid.uuidString).jpg",
lastDisplayPictureUpdate: 1234567891
)
))
}
}
}
}
}

@ -91,6 +91,9 @@ class MessageSenderGroupsSpec: QuickSpec {
groupSessionId: SessionId(.standard, hex: TestConstants.publicKey),
authData: "TestAuthData".data(using: .utf8)!
))
crypto
.when { $0.generate(.tokenSubaccount(config: .any, groupSessionId: .any, memberId: .any)) }
.thenReturn(Array("TestSubAccountToken".data(using: .utf8)!))
crypto
.when { try $0.tryGenerate(.randomBytes(numberBytes: .any)) }
.thenReturn(Data((0..<DisplayPictureManager.aes256KeyByteLength).map { _ in 1 }))
@ -135,12 +138,15 @@ class MessageSenderGroupsSpec: QuickSpec {
return groupMembersConf
}()
@TestState var groupInfoConfig: SessionUtil.Config! = .object(groupInfoConf)
@TestState var groupMembersConfig: SessionUtil.Config! = .object(groupMembersConf)
@TestState var groupKeysConfig: SessionUtil.Config! = {
@TestState var groupKeysConf: UnsafeMutablePointer<config_group_keys>! = {
var groupKeysConf: UnsafeMutablePointer<config_group_keys>!
_ = groups_keys_init(&groupKeysConf, &secretKey, &groupEdPK, &groupEdSK, groupInfoConf, groupMembersConf, nil, 0, nil)
return groupKeysConf
}()
@TestState var groupInfoConfig: SessionUtil.Config! = .object(groupInfoConf)
@TestState var groupMembersConfig: SessionUtil.Config! = .object(groupMembersConf)
@TestState var groupKeysConfig: SessionUtil.Config! = {
return .groupKeys(groupKeysConf, info: groupInfoConf, members: groupMembersConf)
}()
@TestState(cache: .sessionUtil, in: dependencies) var mockSessionUtilCache: MockSessionUtilCache! = MockSessionUtilCache(
@ -206,6 +212,34 @@ class MessageSenderGroupsSpec: QuickSpec {
describe("a MessageSender dealing with Groups") {
// MARK: -- when creating a group
context("when creating a group") {
// MARK: ---- loads the state into the cache
it("loads the state into the cache") {
MessageSender
.createGroup(
name: "TestGroupName",
description: nil,
displayPictureData: nil,
members: [
("051111111111111111111111111111111111111111111111111111111111111111", nil)
],
using: dependencies
)
.sinkAndStore(in: &disposables)
expect(mockSessionUtilCache)
.to(call(.exactly(times: 1), matchingParameters: .atLeast(2)) { cache in
cache.setConfig(for: .groupInfo, sessionId: groupId, to: .any)
})
expect(mockSessionUtilCache)
.to(call(.exactly(times: 1), matchingParameters: .atLeast(2)) { cache in
cache.setConfig(for: .groupMembers, sessionId: groupId, to: .any)
})
expect(mockSessionUtilCache)
.to(call(.exactly(times: 1), matchingParameters: .atLeast(2)) { cache in
cache.setConfig(for: .groupKeys, sessionId: groupId, to: .any)
})
}
// MARK: ---- returns the created thread
it("returns the created thread") {
MessageSender
@ -769,6 +803,362 @@ class MessageSenderGroupsSpec: QuickSpec {
})
}
}
// MARK: -- when adding members to a group
context("when adding members to a group") {
beforeEach {
// Rekey a couple of times to increase the key generation to 1
var fakeHash1: [CChar] = "fakehash1".cArray.nullTerminated()
var fakeHash2: [CChar] = "fakehash2".cArray.nullTerminated()
var pushResult: UnsafePointer<UInt8>? = nil
var pushResultLen: Int = 0
_ = groups_keys_rekey(groupKeysConf, groupInfoConf, groupMembersConf, &pushResult, &pushResultLen)
_ = groups_keys_load_message(groupKeysConf, &fakeHash1, pushResult, pushResultLen, 1234567890, groupInfoConf, groupMembersConf)
_ = groups_keys_rekey(groupKeysConf, groupInfoConf, groupMembersConf, &pushResult, &pushResultLen)
_ = groups_keys_load_message(groupKeysConf, &fakeHash2, pushResult, pushResultLen, 1234567890, groupInfoConf, groupMembersConf)
mockStorage.write { db in
try SessionThread.fetchOrCreate(
db,
id: groupId.hexString,
variant: .group,
shouldBeVisible: true,
calledFromConfigHandling: false,
using: dependencies
)
try ClosedGroup(
threadId: groupId.hexString,
name: "TestGroup",
formationTimestamp: 1234567890,
shouldPoll: true,
groupIdentityPrivateKey: groupSecretKey,
authData: nil,
invited: false
).upsert(db)
}
}
// MARK: ---- does nothing if the current user is not an admin
it("does nothing if the current user is not an admin") {
mockStorage.write { db in
try ClosedGroup
.updateAll(
db,
ClosedGroup.Columns.groupIdentityPrivateKey.set(to: nil)
)
}
MessageSender.addGroupMembers(
groupSessionId: groupId.hexString,
members: [
("051111111111111111111111111111111111111111111111111111111111111112", nil)
],
allowAccessToHistoricMessages: false,
using: dependencies
)
let members: [GroupMember]? = mockStorage.read { db in try GroupMember.fetchAll(db) }
expect(groups_members_size(groupMembersConf)).to(equal(0))
expect(members?.count).to(equal(0))
}
// MARK: ---- adds the member to the database in the sending state
it("adds the member to the database in the sending state") {
MessageSender.addGroupMembers(
groupSessionId: groupId.hexString,
members: [
("051111111111111111111111111111111111111111111111111111111111111112", nil)
],
allowAccessToHistoricMessages: false,
using: dependencies
)
let members: [GroupMember]? = mockStorage.read { db in try GroupMember.fetchAll(db) }
expect(members?.count).to(equal(1))
expect(members?.first?.profileId)
.to(equal("051111111111111111111111111111111111111111111111111111111111111112"))
expect(members?.first?.role).to(equal(.standard))
expect(members?.first?.roleStatus).to(equal(.sending))
}
// MARK: ---- adds the member to GROUP_MEMBERS
it("adds the member to GROUP_MEMBERS") {
MessageSender.addGroupMembers(
groupSessionId: groupId.hexString,
members: [
("051111111111111111111111111111111111111111111111111111111111111112", nil)
],
allowAccessToHistoricMessages: false,
using: dependencies
)
expect(groups_members_size(groupMembersConf)).to(equal(1))
let members: Set<GroupMember>? = try? SessionUtil.extractMembers(
from: groupMembersConf,
groupSessionId: groupId
)
expect(members?.count).to(equal(1))
expect(members?.first?.profileId)
.to(equal("051111111111111111111111111111111111111111111111111111111111111112"))
expect(members?.first?.role).to(equal(.standard))
expect(members?.first?.roleStatus).to(equal(.pending))
}
// MARK: ---- and granting access to historic messages
context("and granting access to historic messages") {
// MARK: ---- performs a supplemental key rotation
it("performs a supplemental key rotation") {
let initialKeyRotation: Int = try SessionUtil.currentGeneration(
groupSessionId: groupId,
using: dependencies
)
MessageSender.addGroupMembers(
groupSessionId: groupId.hexString,
members: [
("051111111111111111111111111111111111111111111111111111111111111112", nil)
],
allowAccessToHistoricMessages: true,
using: dependencies
)
// Can't actually detect a supplemental rotation directly but can check that the
// keys generation didn't increase
let result: Int = try SessionUtil.currentGeneration(
groupSessionId: groupId,
using: dependencies
)
expect(result).to(equal(initialKeyRotation))
}
// MARK: ---- sends the supplemental key rotation data
it("sends the supplemental key rotation data") {
let requestDataString: String = "ZDE6IzI0OhOKDnbpLN3QJVbKzR8mOmjn6gXmeUFdTDE6K" +
"2wxNDA669s6Q2aETGZ5agGXfVVrC8Q9JA4bIoqv5iWyQWjttPhqDK2IZHXGVDZ/Kaz9tEq2Rl" +
"r2B9/neDBUFPtH3haJFN/zkIq1dAIwkgQQ4xJK00zWvZt6HejV1Fy6W9eI1oRJJny0++5+hxp" +
"LPczVOFKOPs+rrB3aUpMsNUnJHOEhW9g6zi/UPjuCWTnnvpxlMTpHaTFlMTp+NjQ6dKi86jZJ" +
"l3oiJEA5h5pBE5oOJHQNvtF8GOcsYwrIFTZKnI7AGkBSu1TxP0xLWwTUzjOGMgmKvlIgkQ6e9" +
"r3JBmU="
let expectedRequest: URLRequest = try SnodeAPI
.preparedSendMessage(
message: SnodeMessage(
recipient: groupId.hexString,
data: requestDataString,
ttl: ConfigDump.Variant.groupKeys.ttl,
timestampMs: UInt64(1234567890000)
),
in: .configGroupKeys,
authMethod: Authentication.groupAdmin(
groupSessionId: groupId,
ed25519SecretKey: Array(groupSecretKey)
),
using: dependencies
)
.request
MessageSender.addGroupMembers(
groupSessionId: groupId.hexString,
members: [
("051111111111111111111111111111111111111111111111111111111111111112", nil)
],
allowAccessToHistoricMessages: true,
using: dependencies
)
// If there is a pending keys config then merge it to complete the process
var pushResult: UnsafePointer<UInt8>? = nil
var pushResultLen: Int = 0
if groups_keys_pending_config(groupKeysConf, &pushResult, &pushResultLen) {
// Rekey a couple of times to increase the key generation to 1
var fakeHash3: [CChar] = "fakehash3".cArray.nullTerminated()
_ = groups_keys_load_message(groupKeysConf, &fakeHash3, pushResult, pushResultLen, 1234567890, groupInfoConf, groupMembersConf)
}
expect(mockNetwork)
.to(call(.exactly(times: 1), matchingParameters: .all) { network in
network.send(
.selectedNetworkRequest(
expectedRequest.httpBody!,
to: dependencies.randomElement(mockSwarmCache)!,
timeout: HTTP.defaultTimeout,
using: .any
)
)
})
}
}
// MARK: ---- and not granting access to historic messages
context("and not granting access to historic messages") {
// MARK: ---- performs a full key rotation
it("performs a full key rotation") {
let initialKeyRotation: Int = try SessionUtil.currentGeneration(
groupSessionId: groupId,
using: dependencies
)
MessageSender.addGroupMembers(
groupSessionId: groupId.hexString,
members: [
("051111111111111111111111111111111111111111111111111111111111111112", nil)
],
allowAccessToHistoricMessages: false,
using: dependencies
)
// If there is a pending keys config then merge it to complete the process
var pushResult: UnsafePointer<UInt8>? = nil
var pushResultLen: Int = 0
if groups_keys_pending_config(groupKeysConf, &pushResult, &pushResultLen) {
// Rekey a couple of times to increase the key generation to 1
var fakeHash3: [CChar] = "fakehash3".cArray.nullTerminated()
_ = groups_keys_load_message(groupKeysConf, &fakeHash3, pushResult, pushResultLen, 1234567890, groupInfoConf, groupMembersConf)
}
let result: Int = try SessionUtil.currentGeneration(
groupSessionId: groupId,
using: dependencies
)
expect(result).to(beGreaterThan(initialKeyRotation))
}
}
// MARK: ---- calls the unrevoke subaccounts endpoint
it("calls the unrevoke subaccounts endpoint") {
let expectedRequest: URLRequest = try SnodeAPI
.preparedUnrevokeSubaccounts(
subaccountsToUnrevoke: [Array("TestSubAccountToken".data(using: .utf8)!)],
authMethod: Authentication.groupAdmin(
groupSessionId: groupId,
ed25519SecretKey: Array(groupSecretKey)
),
using: dependencies
)
.request
MessageSender.addGroupMembers(
groupSessionId: groupId.hexString,
members: [
("051111111111111111111111111111111111111111111111111111111111111112", nil)
],
allowAccessToHistoricMessages: true,
using: dependencies
)
expect(mockNetwork)
.to(call(.exactly(times: 1), matchingParameters: .all) { network in
network.send(
.selectedNetworkRequest(
expectedRequest.httpBody!,
to: dependencies.randomElement(mockSwarmCache)!,
timeout: HTTP.defaultTimeout,
using: .any
)
)
})
}
// MARK: ---- schedules member invite jobs
it("schedules member invite jobs") {
MessageSender.addGroupMembers(
groupSessionId: groupId.hexString,
members: [
("051111111111111111111111111111111111111111111111111111111111111112", nil)
],
allowAccessToHistoricMessages: true,
using: dependencies
)
expect(mockJobRunner)
.to(call(.exactly(times: 1), matchingParameters: .all) { jobRunner in
jobRunner.add(
.any,
job: Job(
variant: .groupInviteMember,
threadId: groupId.hexString,
details: try? GroupInviteMemberJob.Details(
memberSessionIdHexString: "051111111111111111111111111111111111111111111111111111111111111112",
authInfo: .groupMember(
groupSessionId: SessionId(.standard, hex: TestConstants.publicKey),
authData: "TestAuthData".data(using: .utf8)!
)
)
),
dependantJob: nil,
canStartJob: true,
using: .any
)
})
}
// MARK: ---- adds a member change control message
it("adds a member change control message") {
MessageSender.addGroupMembers(
groupSessionId: groupId.hexString,
members: [
("051111111111111111111111111111111111111111111111111111111111111112", nil)
],
allowAccessToHistoricMessages: true,
using: dependencies
)
let interactions: [Interaction]? = mockStorage.read { db in try Interaction.fetchAll(db) }
expect(interactions?.count).to(equal(1))
expect(interactions?.first?.variant).to(equal(.infoGroupMembersUpdated))
expect(interactions?.first?.body).to(equal(
ClosedGroup.MessageInfo
.addedUsers(names: ["0511...1112"])
.infoString(using: dependencies)
))
}
// MARK: ---- schedules sending of the member change message
it("schedules sending of the member change message") {
MessageSender.addGroupMembers(
groupSessionId: groupId.hexString,
members: [
("051111111111111111111111111111111111111111111111111111111111111112", nil)
],
allowAccessToHistoricMessages: true,
using: dependencies
)
expect(mockJobRunner)
.to(call(.exactly(times: 1), matchingParameters: .all) { jobRunner in
jobRunner.add(
.any,
job: Job(
variant: .messageSend,
threadId: groupId.hexString,
interactionId: nil,
details: MessageSendJob.Details(
destination: .closedGroup(groupPublicKey: groupId.hexString),
message: try! GroupUpdateMemberChangeMessage(
changeType: .added,
memberSessionIds: [
"051111111111111111111111111111111111111111111111111111111111111112"
],
sentTimestamp: 1234567890000,
authMethod: Authentication.groupAdmin(
groupSessionId: groupId,
ed25519SecretKey: Array(groupSecretKey)
),
using: dependencies
),
isSyncMessage: false
)
),
dependantJob: nil,
canStartJob: true,
using: .any
)
})
}
}
}
}
}

Loading…
Cancel
Save