├── resources ├── screenshot1.png ├── screenshot2.png ├── screenshot3.png ├── screenshot4.png ├── screenshot5.png ├── screenshot6.png ├── screenshot7.png ├── module_image.png └── js │ ├── humhub.mail.ConversationViewEntry.js │ ├── humhub.mail.notification.min.js │ ├── humhub.mail.conversation.js │ └── humhub.mail.notification.js ├── tests ├── config │ ├── env │ │ ├── chrome..yml │ │ ├── firefox.yml │ │ ├── phantom.yml │ │ └── travis.yml │ ├── api.php │ ├── unit.php │ ├── common.php │ ├── functional.php │ └── test.php ├── codeception │ ├── _output │ │ └── .gitignore │ ├── _support │ │ ├── _generated │ │ │ └── .gitignore │ │ ├── ApiTester.php │ │ ├── UnitTester.php │ │ ├── AcceptanceTester.php │ │ ├── FunctionalTester.php │ │ └── FunctionalMailHelper.php │ ├── config │ │ ├── api.php │ │ ├── unit.php │ │ └── functional.php │ ├── api │ │ ├── _bootstrap.php │ │ ├── UserCest.php │ │ ├── TagCest.php │ │ ├── MessageCest.php │ │ └── EntryCest.php │ ├── acceptance │ │ └── _bootstrap.php │ ├── functional │ │ ├── _bootstrap.php │ │ └── SendMailCest.php │ ├── unit │ │ ├── _bootstrap.php │ │ └── UserMessageTagTest.php │ ├── unit.suite.yml │ ├── fixtures │ │ ├── MessageEntryFixture.php │ │ ├── UserMessageFixture.php │ │ ├── UserMessageTagFixture.php │ │ ├── ConversationTagFixture.php │ │ ├── MessageFixture.php │ │ └── data │ │ │ ├── conversation_tag.php │ │ │ ├── message.php │ │ │ ├── user_message_tag.php │ │ │ ├── message_tag.php │ │ │ ├── user_message.php │ │ │ └── message_entry.php │ ├── api.suite.yml │ ├── functional.suite.yml │ ├── acceptance.suite.yml │ └── _bootstrap.php └── codeception.yml ├── .gitignore ├── docs ├── swagger │ └── build.sh ├── README.md └── DEVELOPER.md ├── .github └── workflows │ ├── php-cs-fixer.yml │ ├── rector-auto-pr.yaml.yml │ ├── marketplace-upload.yml │ ├── codeception-master.yml │ ├── codeception-next.yml │ ├── codeception-develop.yml │ └── codeception-min-version.yml ├── widgets ├── Notifications.php ├── views │ ├── inboxFilterTextInput.php │ ├── conversationEntryMenu.php │ ├── conversationHeader.php │ ├── conversationState.php │ ├── inbox.php │ ├── notificationInbox.php │ ├── inboxMessagePreview.php │ ├── conversationEntry.php │ ├── inboxFilter.php │ └── conversationSettingsMenu.php ├── ConversationHeader.php ├── ManageTagsLink.php ├── NotificationInbox.php ├── InboxFilter.php ├── MailRichtextEditor.php ├── ConversationSettingsMenu.php ├── ConversationTagBadge.php ├── ConversationTags.php ├── ConversationView.php ├── PinLink.php ├── TimeAgo.php ├── MessageEntryTime.php ├── ConversationEntryMenu.php ├── ConversationTagPicker.php ├── Messages.php ├── ConversationDateBadge.php ├── ConversationInbox.php ├── NewMessageButton.php ├── ConversationStateBadge.php ├── ParticipantUserList.php └── ConversationEntry.php ├── assets ├── MailMessengerAsset.php ├── MailStyleAsset.php └── MailNotificationAsset.php ├── notifications ├── MailNotification.php ├── ConversationNotification.php ├── MailNotificationCategory.php └── ConversationNotificationCategory.php ├── views ├── mail │ ├── notificationList.php │ ├── index.php │ ├── adduser.php │ ├── _conversation_sidebar.php │ ├── editEntry.php │ └── create.php ├── tag │ ├── editModal.php │ ├── editConversationTagsModal.php │ └── manage.php ├── emails │ └── plaintext │ │ └── NewMessage.php └── config │ └── index.php ├── migrations ├── uninstall.php ├── m240125_080730_pinned.php ├── m230214_062338_drop_file_id.php ├── m150709_050451_namespace.php ├── m150429_190600_indexes.php ├── m150709_050452_message_tags.php ├── m230213_094842_add_entry_type.php ├── m230919_055432_entry_foreign_key.php ├── m150709_050453_conversation_tags.php └── m131023_165507_initial.php ├── models ├── states │ ├── MessageUserLeft.php │ ├── MessageUserJoined.php │ └── AbstractMessageState.php ├── forms │ ├── AddTag.php │ ├── ConversationTagsForm.php │ ├── ReplyForm.php │ └── InviteParticipantForm.php ├── UserMessageTag.php ├── MessageEntry.php └── UserMessage.php ├── composer.json ├── package.json ├── module.json ├── controllers ├── ConfigController.php ├── InboxController.php ├── rest │ ├── UserController.php │ ├── TagController.php │ └── MessageController.php └── TagController.php ├── permissions ├── StartConversation.php └── SendMail.php ├── live ├── UserMessageDeleted.php └── NewUserMessage.php ├── config.php ├── search ├── SearchRecord.php └── SearchProvider.php ├── helpers ├── RestDefinitions.php └── Url.php ├── Gruntfile.js ├── Module.php └── messages ├── am └── base.php ├── ko └── base.php └── ro └── base.php /resources/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humhub/mail/HEAD/resources/screenshot1.png -------------------------------------------------------------------------------- /resources/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humhub/mail/HEAD/resources/screenshot2.png -------------------------------------------------------------------------------- /resources/screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humhub/mail/HEAD/resources/screenshot3.png -------------------------------------------------------------------------------- /resources/screenshot4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humhub/mail/HEAD/resources/screenshot4.png -------------------------------------------------------------------------------- /resources/screenshot5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humhub/mail/HEAD/resources/screenshot5.png -------------------------------------------------------------------------------- /resources/screenshot6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humhub/mail/HEAD/resources/screenshot6.png -------------------------------------------------------------------------------- /resources/screenshot7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humhub/mail/HEAD/resources/screenshot7.png -------------------------------------------------------------------------------- /resources/module_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/humhub/mail/HEAD/resources/module_image.png -------------------------------------------------------------------------------- /tests/config/env/chrome..yml: -------------------------------------------------------------------------------- 1 | modules: 2 | config: 3 | WebDriver: 4 | browser: 'chrome' -------------------------------------------------------------------------------- /tests/config/env/firefox.yml: -------------------------------------------------------------------------------- 1 | modules: 2 | config: 3 | WebDriver: 4 | browser: 'firefox' -------------------------------------------------------------------------------- /tests/config/env/phantom.yml: -------------------------------------------------------------------------------- 1 | modules: 2 | config: 3 | WebDriver: 4 | browser: 'phantomjs' -------------------------------------------------------------------------------- /tests/codeception/_output/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /tests/codeception/_support/_generated/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore -------------------------------------------------------------------------------- /tests/codeception/config/api.php: -------------------------------------------------------------------------------- 1 | ['mail'], 5 | 'fixtures' => [ 6 | 'default', 7 | 'message' => 'tests\codeception\fixtures\MessageFixture', 8 | ], 9 | ]; 10 | -------------------------------------------------------------------------------- /.github/workflows/php-cs-fixer.yml: -------------------------------------------------------------------------------- 1 | name: PHP CS Fixer 2 | 3 | on: 4 | push: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | fixers: 9 | uses: humhub/module-coding-standards/.github/workflows/php-cs-fixer.yml@main 10 | -------------------------------------------------------------------------------- /tests/config/env/travis.yml: -------------------------------------------------------------------------------- 1 | modules: 2 | config: 3 | WebDriver: 4 | browser: chrome 5 | port: 9515 6 | capabilities: 7 | chromeOptions: 8 | args: ['--headless', '--disable-gpu', '--no-sandbox', '--window-size=1024,768'] -------------------------------------------------------------------------------- /.github/workflows/rector-auto-pr.yaml.yml: -------------------------------------------------------------------------------- 1 | name: Rector 2 | 3 | on: 4 | schedule: 5 | - cron: "0 5 * * 5" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | rector: 10 | uses: humhub/module-coding-standards/.github/workflows/rector-auto-pr.yaml@main 11 | -------------------------------------------------------------------------------- /.github/workflows/marketplace-upload.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - 'v*' 5 | 6 | name: Upload to HumHub Marketplace 7 | 8 | jobs: 9 | build: 10 | uses: humhub/module-coding-standards/.github/workflows/marketplace-upload.yml@main 11 | secrets: inherit -------------------------------------------------------------------------------- /tests/codeception/api/_bootstrap.php: -------------------------------------------------------------------------------- 1 | 14 |
15 | 16 |
17 | -------------------------------------------------------------------------------- /tests/codeception/unit.suite.yml: -------------------------------------------------------------------------------- 1 | # Codeception Test Suite Configuration 2 | 3 | # suite for unit (internal) tests. 4 | # RUN `build` COMMAND AFTER ADDING/REMOVING MODULES. 5 | 6 | actor: UnitTester 7 | modules: 8 | enabled: 9 | - tests\codeception\_support\CodeHelper 10 | - Yii2 11 | config: 12 | Yii2: 13 | configFile: 'codeception/config/unit.php' 14 | transaction: false 15 | -------------------------------------------------------------------------------- /tests/codeception/fixtures/MessageEntryFixture.php: -------------------------------------------------------------------------------- 1 | render('conversationHeader', ['message' => $this->message]); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /widgets/views/conversationEntryMenu.php: -------------------------------------------------------------------------------- 1 | 10 |
11 | 12 |
13 | 14 |
15 | 16 |
17 | -------------------------------------------------------------------------------- /assets/MailMessengerAsset.php: -------------------------------------------------------------------------------- 1 | false, 13 | ]; 14 | 15 | public $js = [ 16 | 'humhub.mail.messenger.bundle.js', 17 | ]; 18 | 19 | public $depends = [ 20 | MailNotificationAsset::class, 21 | ]; 22 | } 23 | -------------------------------------------------------------------------------- /assets/MailStyleAsset.php: -------------------------------------------------------------------------------- 1 | false, 19 | ]; 20 | 21 | public $css = [ 22 | 'humhub.mail.min.css', 23 | ]; 24 | } 25 | -------------------------------------------------------------------------------- /notifications/MailNotification.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | $userMessage]) ?> 11 | 12 | 13 |
14 | 15 | -------------------------------------------------------------------------------- /tests/codeception/api.suite.yml: -------------------------------------------------------------------------------- 1 | # Codeception Test Suite Configuration 2 | 3 | # suite for REST API tests. 4 | # RUN `build` COMMAND AFTER ADDING/REMOVING MODULES. 5 | 6 | actor: ApiTester 7 | modules: 8 | enabled: 9 | - REST 10 | - Yii2 11 | - tests\codeception\_support\DynamicFixtureHelper 12 | config: 13 | REST: 14 | url: 'http://localhost:8080/api/v1/' 15 | depends: Yii2 16 | part: Json 17 | Yii2: 18 | configFile: 'codeception/config/api.php' 19 | -------------------------------------------------------------------------------- /notifications/ConversationNotification.php: -------------------------------------------------------------------------------- 1 | setType(static::TYPE_NONE) 15 | ->setText(Yii::t('MailModule.base', 'Manage Tags')) 16 | ->link(Url::toManageTags()) 17 | ->icon('gear')->right()->cssClass('manage-tags-link'); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /widgets/NotificationInbox.php: -------------------------------------------------------------------------------- 1 | render('notificationInbox', [ 19 | 'newMailMessageCount' => UserMessage::getNewMessageCount(), 20 | ]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /widgets/views/conversationHeader.php: -------------------------------------------------------------------------------- 1 | 10 |

title) . ' ' . $message->getPinIcon() ?>

11 | 12 |
13 | $message]) ?> 14 |
15 | 16 | $message]) ?> 17 | -------------------------------------------------------------------------------- /tests/codeception/fixtures/UserMessageTagFixture.php: -------------------------------------------------------------------------------- 1 | safeDropTable('user_message_tag'); 10 | $this->safeDropTable('message_tag'); 11 | $this->safeDropTable('user_message'); 12 | $this->safeDropTable('message_entry'); 13 | $this->safeDropTable('message'); 14 | } 15 | 16 | public function down() 17 | { 18 | echo "uninstall does not support migration down.\n"; 19 | return false; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /tests/codeception/fixtures/ConversationTagFixture.php: -------------------------------------------------------------------------------- 1 | render('inboxFilter', ['options' => $this->getOptions(), 'model' => $this->model]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /views/tag/editModal.php: -------------------------------------------------------------------------------- 1 | 12 | 13 | Yii::t('MailModule.base', 'Edit tag'), 15 | 'footer' => ModalButton::cancel() . ' ' . ModalButton::save()->submit(Url::toEditTag($model->id)), 16 | ])?> 17 | field($model, 'name') ?> 18 | 19 | -------------------------------------------------------------------------------- /tests/codeception/functional.suite.yml: -------------------------------------------------------------------------------- 1 | # Codeception Test Suite Configuration 2 | 3 | # suite for functional (integration) tests. 4 | # emulate web requests and make application process them. 5 | # (tip: better to use with frameworks). 6 | 7 | # RUN `build` COMMAND AFTER ADDING/REMOVING MODULES. 8 | actor: FunctionalTester 9 | modules: 10 | enabled: 11 | - Filesystem 12 | - Yii2 13 | - tests\codeception\_support\TestHelper 14 | - tests\codeception\_support\HumHubHelper 15 | - mail\FunctionalMailHelper 16 | config: 17 | Yii2: 18 | configFile: 'codeception/config/functional.php' 19 | -------------------------------------------------------------------------------- /widgets/views/conversationState.php: -------------------------------------------------------------------------------- 1 | 15 | 16 | $entry]) ?> 17 | 18 | 19 | $entry]) ?> -------------------------------------------------------------------------------- /models/states/MessageUserLeft.php: -------------------------------------------------------------------------------- 1 | safeAddColumn(UserMessage::tableName(), 'pinned', $this->boolean()->defaultValue(false)->notNull()); 17 | } 18 | 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function safeDown() 23 | { 24 | $this->safeDropColumn(UserMessage::tableName(), 'pinned'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /models/states/MessageUserJoined.php: -------------------------------------------------------------------------------- 1 | safeDropColumn(MessageEntry::tableName(), 'file_id'); 17 | } 18 | 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function safeDown() 23 | { 24 | echo "m230214_062338_drop_file_id cannot be reverted.\n"; 25 | 26 | return false; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "humhub/mail", 3 | "type": "humhub-module", 4 | "config": { 5 | "platform": { 6 | "php": "8.2" 7 | } 8 | }, 9 | "repositories": { 10 | "humhub-module-coding-standards": { 11 | "type": "vcs", 12 | "url": "https://github.com/humhub/module-coding-standards.git" 13 | } 14 | }, 15 | "require-dev": { 16 | "humhub/module-coding-standards": "dev-main" 17 | }, 18 | "scripts": { 19 | "rector": "vendor/bin/rector process --config=vendor/humhub/module-coding-standards/rector.php", 20 | "fixer": "vendor/bin/php-cs-fixer fix --config=vendor/humhub/module-coding-standards/php-cs-fixer.php" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /widgets/MailRichtextEditor.php: -------------------------------------------------------------------------------- 1 | layout = static::LAYOUT_INLINE; 19 | $this->placeholder = Yii::t('MailModule.base', 'Write a message...'); 20 | parent::init(); // TODO: Change the autogenerated stub 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/codeception/_support/ApiTester.php: -------------------------------------------------------------------------------- 1 | renameClass('MessageEntry', MessageEntry::className()); 12 | } 13 | 14 | public function down() 15 | { 16 | echo "m150709_050451_namespace cannot be reverted.\n"; 17 | 18 | return false; 19 | } 20 | 21 | /* 22 | // Use safeUp/safeDown to run migration code within a transaction 23 | public function safeUp() 24 | { 25 | } 26 | 27 | public function safeDown() 28 | { 29 | } 30 | */ 31 | } 32 | -------------------------------------------------------------------------------- /widgets/views/inbox.php: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | $userMessage]) ?> 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/codeception/_support/UnitTester.php: -------------------------------------------------------------------------------- 1 | tag = new MessageTag(['user_id' => Yii::$app->user->id]); 21 | parent::init(); 22 | } 23 | 24 | public function load($data, $formName = null) 25 | { 26 | return $this->tag->load($data, $formName); 27 | } 28 | 29 | public function save() 30 | { 31 | return $this->tag->save(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /views/emails/plaintext/NewMessage.php: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | : 27 | -------------------------------------------------------------------------------- /tests/codeception/_support/AcceptanceTester.php: -------------------------------------------------------------------------------- 1 | render('conversationSettingsMenu', [ 23 | 'message' => $this->message, 24 | 'isSingleParticipant' => $this->message->getUsers()->count() === 1, 25 | 'canAddParticipant' => Yii::$app->user->can(StartConversation::class), 26 | ]); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /views/mail/index.php: -------------------------------------------------------------------------------- 1 | 13 |
14 |
15 |
16 | render('_conversation_sidebar') ?> 17 |
18 | 19 |
20 | $messageId]) ?> 21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /docs/DEVELOPER.md: -------------------------------------------------------------------------------- 1 | Developer 2 | ========= 3 | 4 | Build 5 | ----- 6 | 7 | In order to build script or stylesheet run: 8 | 9 | Install npm packages: 10 | 11 | ``` 12 | npm install 13 | ``` 14 | 15 | Manually build: 16 | 17 | ``` 18 | grunt build 19 | ``` 20 | 21 | Watch for file changes: 22 | 23 | ``` 24 | grunt watch 25 | ``` 26 | 27 | > Note: While development you should set the `forceCopy` publish option of the mail asset bundles to true. 28 | 29 | 30 | REST API 31 | -------- 32 | 33 | The Mail module also has integration to the [REST API](https://www.humhub.com/en/marketplace/rest/). 34 | 35 | The [documentation](https://www.humhub.com/en/marketplace/mail/docs/swagger/mail.html) as HTML can be found here. 36 | 37 | You can also find Swagger (OpenAPI 3.0.0) documentation files in the `/docs/swagger` directory of this module. -------------------------------------------------------------------------------- /tests/codeception/fixtures/data/conversation_tag.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | Yii::t('MailModule.base', 'Add participants'), 12 | 'form' => ['enableClientValidation' => false], 13 | 'footer' => ModalButton::cancel() . ' ' . ModalButton::save(Yii::t('MailModule.base', 'Confirm'))->action('addUser', $inviteForm->getUrl(), '#mail-conversation-root')->close(), 14 | ])?> 15 |
16 | field($inviteForm, 'recipients')->widget(UserPickerField::class, [ 17 | 'url' => $inviteForm->getPickerUrl(), 18 | 'focus' => true, 19 | ])->label(false) ?> 20 |
21 | 22 | -------------------------------------------------------------------------------- /module.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "mail", 3 | "name": "Messenger", 4 | "description": "Engage in private conversations with colleagues and teams through secure messaging.", 5 | "keywords": [ 6 | "mail", 7 | "messaging", 8 | "messenger", 9 | "communication" 10 | ], 11 | "version": "3.3.6", 12 | "humhub": { 13 | "minVersion": "1.18.0-beta.6" 14 | }, 15 | "homepage": "https://github.com/humhub/mail", 16 | "authors": [ 17 | { 18 | "name": "Andreas Strobel" 19 | }, 20 | { 21 | "name": "Lucas Bartholemy" 22 | }, 23 | { 24 | "name": "Julian Harrer" 25 | } 26 | ], 27 | "screenshots": [ 28 | "resources/screenshot1.png", 29 | "resources/screenshot2.png", 30 | "resources/screenshot3.png", 31 | "resources/screenshot4.png", 32 | "resources/screenshot5.png", 33 | "resources/screenshot6.png", 34 | "resources/screenshot7.png" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /tests/codeception/_support/FunctionalMailHelper.php: -------------------------------------------------------------------------------- 1 | getModule('Yii2')->_loadPage( 18 | 'POST', 19 | 'index-test.php?r=mail/mail/create', 20 | ['CreateMessage[recipient][]' => $recipient, 21 | 'CreateMessage[title]' => $title, 22 | 'CreateMessage[message]' => $message], 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /controllers/ConfigController.php: -------------------------------------------------------------------------------- 1 | load(Yii::$app->request->post()) && $form->save()) { 31 | $this->view->saved(); 32 | } 33 | 34 | return $this->render('index', ['model' => $form]); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /models/forms/ConversationTagsForm.php: -------------------------------------------------------------------------------- 1 | tags)) { 24 | $this->tags = MessageTag::findByMessage(Yii::$app->user->id, $this->message)->all(); 25 | } 26 | } 27 | 28 | public function rules() 29 | { 30 | return [ 31 | ['tags', 'safe'], 32 | ]; 33 | } 34 | 35 | public function save() 36 | { 37 | MessageTag::attach(Yii::$app->user->id, $this->message, $this->tags); 38 | 39 | return true; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /views/tag/editConversationTagsModal.php: -------------------------------------------------------------------------------- 1 | 14 | 15 | Yii::t('MailModule.base', 'Edit conversation tags'), 17 | 'footer' => ModalButton::cancel() . ' ' . ModalButton::save()->submit(Url::toEditConversationTags($model->message)), 18 | ]) ?> 19 |
20 | 21 |
22 | field($model, 'tags')->widget(ConversationTagPicker::class)->label(false) ?> 23 | 24 | -------------------------------------------------------------------------------- /migrations/m150429_190600_indexes.php: -------------------------------------------------------------------------------- 1 | createIndex('index_user_id', 'message_entry', 'user_id', false); 10 | $this->createIndex('index_message_id', 'message_entry', 'message_id', false); 11 | $this->createIndex('index_updated', 'message', 'updated_at', false); 12 | $this->createIndex('index_last_viewed', 'user_message', 'last_viewed', false); 13 | $this->createIndex('index_updated_by', 'message', 'updated_by', false); 14 | } 15 | 16 | public function down() 17 | { 18 | echo "m150429_190600_indexes does not support migration down.\n"; 19 | return false; 20 | } 21 | 22 | /* 23 | // Use safeUp/safeDown to do migration with transaction 24 | public function safeUp() 25 | { 26 | } 27 | 28 | public function safeDown() 29 | { 30 | } 31 | */ 32 | } 33 | -------------------------------------------------------------------------------- /migrations/m150709_050452_message_tags.php: -------------------------------------------------------------------------------- 1 | createTable('message_tag', [ 12 | 'id' => $this->primaryKey(), 13 | 'user_id' => $this->integer()->notNull(), 14 | 'name' => $this->string(100)->notNull(), 15 | 'sort_order' => $this->integer(11)->defaultValue(0), 16 | 'color' => $this->string(7)->null(), 17 | ]); 18 | 19 | try { 20 | $this->addForeignKey('fk-message-tag-user-id', 'message_tag', 'user_id', 'user', 'id', 'cascade'); 21 | } catch (\Exception $e) { 22 | Yii::error($e); 23 | } 24 | } 25 | 26 | public function safeDown() 27 | { 28 | echo "m150709_050452_message_tags cannot be reverted.\n"; 29 | 30 | return false; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /permissions/StartConversation.php: -------------------------------------------------------------------------------- 1 | false, 20 | ]; 21 | 22 | public $js = [ 23 | 'humhub.mail.notification.min.js', 24 | ]; 25 | 26 | public $depends = [ 27 | MailStyleAsset::class, 28 | ]; 29 | 30 | public static function register($view) 31 | { 32 | $view->registerJsConfig([ 33 | 'mail.notification' => [ 34 | 'url' => [ 35 | 'count' => Url::toMessageCountUpdate(), 36 | 'list' => Url::toNotificationList(), 37 | ], 38 | ], 39 | ]); 40 | 41 | return parent::register($view); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/codeception.yml: -------------------------------------------------------------------------------- 1 | actor: Tester 2 | namespace: mail 3 | coverage: 4 | c3_url: 'http://localhost:8080/index-test.php' 5 | enabled: true 6 | include: 7 | - ../models/* 8 | - ../notifications/* 9 | - ../permissions/* 10 | bootstrap: _bootstrap.php 11 | settings: 12 | suite_class: \PHPUnit_Framework_TestSuite 13 | colors: true 14 | shuffle: false 15 | memory_limit: 1024M 16 | log: true 17 | 18 | # This value controls whether PHPUnit attempts to backup global variables 19 | # See https://phpunit.de/manual/current/en/appendixes.annotations.html#appendixes.annotations.backupGlobals 20 | backup_globals: true 21 | paths: 22 | tests: codeception 23 | output: codeception/_output 24 | data: codeception/_data 25 | helpers: codeception/_support 26 | envs: ../../../humhub/tests/config/env 27 | config: 28 | # the entry script URL (with host info) for functional and acceptance tests 29 | # PLEASE ADJUST IT TO THE ACTUAL ENTRY SCRIPT URL 30 | test_entry_url: http://localhost:8080/index-test.php 31 | -------------------------------------------------------------------------------- /widgets/ConversationTagBadge.php: -------------------------------------------------------------------------------- 1 | name)->icon('star') 17 | ->withLink(Link::withAction(null, 'mail.inbox.setTagFilter')->options([ 18 | 'data-tag-id' => $tag->id, 19 | 'data-tag-name' => $tag->name, 20 | 'data-tag-image' => Icon::get('star'), 21 | ])); 22 | } 23 | 24 | public static function getEditConversationTagBadge(Message $message, $icon = 'pencil') 25 | { 26 | return static::light()->icon($icon) 27 | ->withLink(Link::withAction(null, 'ui.modal.load', Url::toEditConversationTags($message))); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /widgets/ConversationTags.php: -------------------------------------------------------------------------------- 1 | user->id, $this->message)->all(); 19 | 20 | $result = Html::beginTag('div', ['id' => static::ID, 'class' => 'panel-body', 'style' => ['display' => count($tags) ? 'block' : 'none']]); 21 | 22 | $result .= '' . Yii::t('MailModule.base', 'My Tags') . ''; 23 | 24 | foreach ($tags as $tag) { 25 | $result .= ConversationTagBadge::get($tag) . ' '; 26 | } 27 | 28 | $result .= ConversationTagBadge::getEditConversationTagBadge($this->message, (empty($tags) ? 'plus' : 'pencil')); 29 | $result .= Html::endTag('div'); 30 | 31 | return $result; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /widgets/ConversationView.php: -------------------------------------------------------------------------------- 1 | $this->messageId, 42 | 'load-message-url' => Url::toLoadMessage(), 43 | 'load-update-url' => Url::toUpdateMessage(), 44 | 'load-more-url' => Url::toLoadMoreMessages(), 45 | 'mark-seen-url' => Url::toNotificationSeen(), 46 | ]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /migrations/m230213_094842_add_entry_type.php: -------------------------------------------------------------------------------- 1 | safeAddColumn( 18 | AbstractMessageEntry::tableName(), 19 | 'type', 20 | $this->tinyInteger() 21 | ->defaultValue(MessageEntry::type()) 22 | ->notNull() 23 | ->unsigned() 24 | ->after('content'), 25 | ); 26 | $this->alterColumn(AbstractMessageEntry::tableName(), 'content', $this->text()->null()); 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function safeDown() 33 | { 34 | $this->safeDropColumn(AbstractMessageEntry::tableName(), 'type'); 35 | $this->alterColumn(AbstractMessageEntry::tableName(), 'content', $this->text()->notNull()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /migrations/m230919_055432_entry_foreign_key.php: -------------------------------------------------------------------------------- 1 | execute('DELETE message_entry FROM message_entry 16 | LEFT JOIN user ON user.id = message_entry.user_id 17 | LEFT JOIN message ON message.id = message_entry.message_id 18 | WHERE user.id IS NULL OR message.id IS NULL'); 19 | $this->safeAddForeignKey('fk-message-entry-user-id', 'message_entry', 'user_id', 'user', 'id', 'CASCADE'); 20 | $this->safeAddForeignKey('fk-message-entry-message-id', 'message_entry', 'message_id', 'message', 'id', 'CASCADE'); 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function safeDown() 27 | { 28 | $this->safeDropForeignKey('fk-message-entry-user-id', 'message_entry'); 29 | $this->safeDropForeignKey('fk-message-entry-message-id', 'message_entry'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /live/UserMessageDeleted.php: -------------------------------------------------------------------------------- 1 | visibility = Content::VISIBILITY_OWNER; 49 | $this->count = UserMessage::getNewMessageCount($this->user_id); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /widgets/PinLink.php: -------------------------------------------------------------------------------- 1 | message->isPinned()) { 27 | return Link::none(Yii::t('MailModule.base', 'Unpin')) 28 | ->action('mail.conversation.linkAction', Url::toUnpinConversation($this->message)) 29 | ->cssClass('dropdown-item') 30 | ->icon('map-pin'); 31 | } 32 | 33 | return Link::none(Yii::t('MailModule.base', 'Pin')) 34 | ->action('mail.conversation.linkAction', Url::toPinConversation($this->message)) 35 | ->cssClass('dropdown-item') 36 | ->icon('map-pin'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/codeception/fixtures/data/message.php: -------------------------------------------------------------------------------- 1 | 1, 'title' => 'First message title', 'created_by' => 1, 'created_at' => '2021-03-15 14:38:49'], 22 | ['id' => 2, 'title' => 'Second message title', 'created_by' => 1, 'created_at' => '2021-03-15 14:38:49'], 23 | ['id' => 3, 'title' => 'Third message title', 'created_by' => 1, 'created_at' => '2021-03-15 14:38:49'], 24 | ]; 25 | -------------------------------------------------------------------------------- /widgets/TimeAgo.php: -------------------------------------------------------------------------------- 1 | params['formatter']['timeAgoHideTimeAfter'])) { 16 | $timeAgoHideTimeAfter = Yii::$app->params['formatter']['timeAgoHideTimeAfter']; 17 | Yii::$app->params['formatter']['timeAgoHideTimeAfter'] = false; 18 | 19 | $result = parent::renderDateTime($elapsed); 20 | 21 | Yii::$app->params['formatter']['timeAgoHideTimeAfter'] = $timeAgoHideTimeAfter; 22 | return $result; 23 | } 24 | 25 | return parent::renderDateTime($elapsed); 26 | } 27 | 28 | public function renderTimeAgo() 29 | { 30 | $result = parent::renderTimeAgo(); 31 | 32 | if ($this->badge) { 33 | $result = '' . $result . ''; 34 | } 35 | 36 | return $result; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/codeception/fixtures/data/user_message_tag.php: -------------------------------------------------------------------------------- 1 | 1, 'user_id' => 1, 'tag_id' => 1], 22 | ['message_id' => 1, 'user_id' => 1, 'tag_id' => 2], 23 | ['message_id' => 1, 'user_id' => 1, 'tag_id' => 3], 24 | ['message_id' => 2, 'user_id' => 2, 'tag_id' => 4], 25 | ['message_id' => 2, 'user_id' => 2, 'tag_id' => 5], 26 | ['message_id' => 3, 'user_id' => 1, 'tag_id' => 6], 27 | ]; 28 | -------------------------------------------------------------------------------- /notifications/MailNotificationCategory.php: -------------------------------------------------------------------------------- 1 | id, 26 | ]; 27 | } 28 | 29 | /** 30 | * Returns a human readable title of this category 31 | */ 32 | public function getTitle() 33 | { 34 | return Yii::t('MailModule.base', 'Message'); 35 | } 36 | 37 | /** 38 | * Returns a group description 39 | */ 40 | public function getDescription() 41 | { 42 | return Yii::t('MailModule.base', 'Receive Notifications when someone sends you a message.'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /models/states/AbstractMessageState.php: -------------------------------------------------------------------------------- 1 | save(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/codeception/fixtures/data/message_tag.php: -------------------------------------------------------------------------------- 1 | 1, 'user_id' => 1, 'name' => 'Tag admin 1', 'sort_order' => 10], 22 | ['id' => 2, 'user_id' => 1, 'name' => 'Tag admin 2', 'sort_order' => 20], 23 | ['id' => 3, 'user_id' => 1, 'name' => 'Tag admin 3', 'sort_order' => 30], 24 | ['id' => 4, 'user_id' => 2, 'name' => 'Tag user1 1', 'sort_order' => 10], 25 | ['id' => 5, 'user_id' => 2, 'name' => 'Tag user2 2', 'sort_order' => 20], 26 | ['id' => 6, 'user_id' => 3, 'name' => 'Tag user3 1', 'sort_order' => 30], 27 | ]; 28 | -------------------------------------------------------------------------------- /views/mail/_conversation_sidebar.php: -------------------------------------------------------------------------------- 1 | user->can(StartConversation::class); 11 | 12 | $filterModel = new InboxFilterForm(); 13 | ?> 14 |
15 |
16 | 17 | 18 | 19 | 20 | 'plus', 'label' => '', 'id' => 'mail-conversation-create-button'])?> 21 | 22 | 23 |
24 | $filterModel]) ?> 25 |
26 | 27 |
28 | 29 |
30 |
31 | $filterModel]) ?> 32 |
33 |
34 | -------------------------------------------------------------------------------- /migrations/m150709_050453_conversation_tags.php: -------------------------------------------------------------------------------- 1 | createTable('user_message_tag', [ 13 | 'message_id' => $this->integer()->notNull(), 14 | 'user_id' => $this->integer()->notNull(), 15 | 'tag_id' => $this->integer()->notNull(), 16 | ], ''); 17 | 18 | $this->addPrimaryKey('pk-user-message-tag', 'user_message_tag', ['user_id', 'message_id', 'tag_id']); 19 | 20 | try { 21 | $this->addForeignKey('fk-user-message-id', 'user_message_tag', ['message_id', 'user_id'], 'user_message', ['message_id', 'user_id'], 'cascade'); 22 | } catch (\Exception $e) { 23 | Yii::error($e, 'mail'); 24 | } 25 | 26 | try { 27 | $this->addForeignKey('fk-conversation-tag-tag-id', 'user_message_tag', 'tag_id', 'message_tag', 'id', 'cascade'); 28 | } catch (\Exception $e) { 29 | Yii::error($e, 'mail'); 30 | } 31 | } 32 | 33 | public function safeDown() 34 | { 35 | echo "m150709_050453_conversation_tags cannot be reverted.\n"; 36 | 37 | return false; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/codeception/_bootstrap.php: -------------------------------------------------------------------------------- 1 | $testRoot]); 16 | codecept_debug('Module root: ' . $testRoot); 17 | 18 | $humhubPath = getenv('HUMHUB_PATH'); 19 | if ($humhubPath === false) { 20 | // If no environment path was set, we assume residing in default the modules directory 21 | $moduleConfig = require $testRoot . '/config/test.php'; 22 | if (isset($moduleConfig['humhub_root'])) { 23 | $humhubPath = $moduleConfig['humhub_root']; 24 | } else { 25 | $humhubPath = dirname(__DIR__, 5); 26 | } 27 | } 28 | 29 | \Codeception\Configuration::append(['humhub_root' => $humhubPath]); 30 | codecept_debug('HumHub Root: ' . $humhubPath); 31 | 32 | // Load test configuration (/config/test.php or /config/env//test.php 33 | $globalConfig = require $humhubPath . '/protected/humhub/tests/codeception/_loadConfig.php'; 34 | 35 | // Load default test bootstrap (initialize Yii...) 36 | require $globalConfig['humhub_root'] . '/protected/humhub/tests/codeception/_bootstrap.php'; 37 | -------------------------------------------------------------------------------- /live/NewUserMessage.php: -------------------------------------------------------------------------------- 1 | visibility = Content::VISIBILITY_OWNER; 51 | $this->count = UserMessage::getNewMessageCount(User::findOne(['guid' => $this->user_guid])); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /widgets/MessageEntryTime.php: -------------------------------------------------------------------------------- 1 | 'conversation-entry-time']; 20 | public array $timeOptions = []; 21 | public array $statusOptions = []; 22 | public string $statusSeparator = ' - '; 23 | 24 | /** 25 | * @inheritdoc 26 | */ 27 | public function run() 28 | { 29 | return Html::tag('div', $this->renderStatus() . $this->renderTime(), $this->options); 30 | } 31 | 32 | protected function renderTime(): string 33 | { 34 | return Html::tag('time', Yii::$app->formatter->asTime($this->entry->created_at, 'short'), $this->timeOptions); 35 | } 36 | 37 | protected function renderStatus(): string 38 | { 39 | if ($this->entry->created_at == $this->entry->updated_at) { 40 | return ''; 41 | } 42 | 43 | return Html::tag('span', Yii::t('MailModule.base', 'edited') . $this->statusSeparator, $this->statusOptions); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /notifications/ConversationNotificationCategory.php: -------------------------------------------------------------------------------- 1 | id, 33 | ]; 34 | } 35 | 36 | /** 37 | * Returns a human readable title of this category 38 | */ 39 | public function getTitle() 40 | { 41 | return Yii::t('MailModule.base', 'Conversation'); 42 | } 43 | 44 | /** 45 | * Returns a group description 46 | */ 47 | public function getDescription() 48 | { 49 | return Yii::t('MailModule.base', 'Receive Notifications when someone opens a new conversation.'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /resources/js/humhub.mail.notification.min.js: -------------------------------------------------------------------------------- 1 | humhub.module("mail.notification",function(o,e,a){function n(){i.get(o.config.url.count).then(function(e){c(parseInt(e.newMessages))})}var s,i=e("client"),r=e("ui.loader"),t=e("event"),l=e("ui.widget").Widget,u=0,c=(o.initOnPjaxLoad=!0,function(e){var n=a("#badge-messages");e&&0!==parseInt(e)?(n.removeClass("d-none"),u=e,n.empty(),n.append(e),n.fadeIn("fast")):(n.addClass("d-none"),u=0),t.trigger("humhub:modules:notification:UpdateTitleNotificationCount")});o.export({init:function(e){e||(t.on("humhub:modules:mail:live:NewUserMessage",function(e,n){n=n[n.length-1];c(n.data.count)}).on("humhub:modules:mail:live:UserMessageDeleted",function(e,n){n=n[n.length-1];c(n.data.count)}),a("#icon-messages").click(function(){s&&s.abort();const n=a("#loader_messages"),t=n.parent();n.parent().find(":not(#loader_messages)").remove(),r.set(n.removeClass("d-none")),i.get(o.config.url.list,{beforeSend:function(e){s=e}}).then(function(e){s=void 0,t.prepend(a(e.html)),n.addClass("d-none"),t.niceScroll({cursorwidth:"7",cursorborder:"",cursorcolor:"#555",cursoropacitymax:"0.2",nativeparentscrolling:!1,railpadding:{top:0,right:3,left:0,bottom:0}})})})),n()},loadMessage:function(e){var n=l.instance("#mail-conversation-root");n&&"function"==typeof n.loadMessage?(n.loadMessage(e),n.$.closest(".container").addClass("mail-conversation-single-message")):i.redirect(e.url),e.finish()},setMailMessageCount:c,updateCount:n,getNewMessageCount:function(){return u}})}); -------------------------------------------------------------------------------- /config.php: -------------------------------------------------------------------------------- 1 | 'mail', 11 | 'class' => 'humhub\modules\mail\Module', 12 | 'namespace' => 'humhub\modules\mail', 13 | 'events' => [ 14 | ['class' => User::class, 'event' => User::EVENT_BEFORE_DELETE, 'callback' => ['humhub\modules\mail\Events', 'onUserDelete']], 15 | ['class' => TopMenu::class, 'event' => TopMenu::EVENT_INIT, 'callback' => ['humhub\modules\mail\Events', 'onTopMenuInit']], 16 | ['class' => NotificationArea::class, 'event' => NotificationArea::EVENT_INIT, 'callback' => ['humhub\modules\mail\Events', 'onNotificationAddonInit']], 17 | ['class' => HeaderControlsMenu::class, 'event' => HeaderControlsMenu::EVENT_INIT, 'callback' => ['humhub\modules\mail\Events', 'onProfileHeaderControlsMenuInit']], 18 | ['class' => IntegrityController::class, 'event' => IntegrityController::EVENT_ON_RUN, 'callback' => ['humhub\modules\mail\Events', 'onIntegrityCheck']], 19 | ['class' => 'humhub\modules\rest\Module', 'event' => 'restApiAddRules', 'callback' => ['humhub\modules\mail\Events', 'onRestApiAddRules']], 20 | ['class' => 'humhub\widgets\MetaSearchWidget', 'event' => 'init', 'callback' => ['humhub\modules\mail\Events', 'onMetaSearchWidgetInit']], 21 | ], 22 | ]; 23 | -------------------------------------------------------------------------------- /widgets/ConversationEntryMenu.php: -------------------------------------------------------------------------------- 1 | initMenus(); 33 | } 34 | 35 | public function initMenus() 36 | { 37 | if ($this->entry->canEdit()) { 38 | $this->menus[] = ModalButton::none()->link() 39 | ->icon('pencil') 40 | ->tooltip(Yii::t('MailModule.base', 'Edit')) 41 | ->load(Url::toEditMessageEntry($this->entry)) 42 | ->cssClass('conversation-edit-button time badge'); 43 | } 44 | } 45 | 46 | /** 47 | * @inheritdoc 48 | */ 49 | public function run() 50 | { 51 | if (empty($this->menus)) { 52 | return ''; 53 | } 54 | 55 | return $this->render('conversationEntryMenu', ['menus' => $this->menus]); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /permissions/SendMail.php: -------------------------------------------------------------------------------- 1 | 1, 'user_id' => 1, 'is_originator' => 1, 'created_by' => 1, 'created_at' => '2021-03-15 14:38:49'], 22 | ['message_id' => 2, 'user_id' => 1, 'is_originator' => 1, 'created_by' => 1, 'created_at' => '2021-03-15 14:38:49'], 23 | ['message_id' => 2, 'user_id' => 2, 'is_originator' => 0, 'created_by' => 2, 'created_at' => '2021-03-15 14:38:49'], 24 | ['message_id' => 3, 'user_id' => 1, 'is_originator' => 1, 'created_by' => 1, 'created_at' => '2021-03-15 14:38:49'], 25 | ['message_id' => 3, 'user_id' => 2, 'is_originator' => 0, 'created_by' => 2, 'created_at' => '2021-03-15 14:38:49'], 26 | ['message_id' => 3, 'user_id' => 3, 'is_originator' => 0, 'created_by' => 3, 'created_at' => '2021-03-15 14:38:49'], 27 | ]; 28 | -------------------------------------------------------------------------------- /tests/codeception/functional/SendMailCest.php: -------------------------------------------------------------------------------- 1 | amUser(); 12 | $I->wantToTest('if sending messages works'); 13 | 14 | $I->amGoingTo('send a message to another user'); 15 | $I->sendMessage('01e50e0d-82cd-41fc-8b0c-552392f5839e', 'TestTitle', 'TestMessage'); 16 | $I->expect('the new message in the database'); 17 | $I->seeRecord('humhub\modules\mail\models\Message', ['title' => 'TestTitle']); 18 | $message = $I->grabRecord('humhub\modules\mail\models\Message', ['title' => 'TestTitle']); 19 | $I->seeRecord('humhub\modules\mail\models\MessageEntry', ['message_id' => $message->id, 'content' => 'TestMessage']); 20 | $I->seeRecord('humhub\modules\mail\models\UserMessage', ['message_id' => $message->id, 'user_id' => 2, 'is_originator' => 1]); 21 | $I->seeRecord('humhub\modules\mail\models\UserMessage', ['message_id' => $message->id, 'user_id' => 3]); 22 | 23 | $I->amGoingTo('check my conversation overview'); 24 | $I->amOnRoute('/mail/mail/index'); 25 | 26 | $I->expect('to see the new message'); 27 | $I->see('Conversation'); 28 | $I->dontSee('There are no messages yet.'); 29 | $I->see('TestTitle'); 30 | $I->see('TestMessage'); 31 | } 32 | 33 | // send mail to multiple recipients 34 | // permissions 35 | // friendship 36 | // Add user 37 | // Delete Message 38 | // Notification 39 | } 40 | -------------------------------------------------------------------------------- /widgets/ConversationTagPicker.php: -------------------------------------------------------------------------------- 1 | defaultResults = MessageTag::findByUser(Yii::$app->user->id)->all(); 32 | } 33 | 34 | /** 35 | * Used to retrieve the option text of a given $item. 36 | * 37 | * @param MessageTag $item selected item 38 | * @return string item option text 39 | */ 40 | protected function getItemText($item) 41 | { 42 | return $item->name; 43 | } 44 | 45 | /** 46 | * Used to retrieve the option image url of a given $item. 47 | * 48 | * @param UserMessageTag $item selected item 49 | * @return string|null image url or null if no selection image required. 50 | * @throws \Exception 51 | */ 52 | protected function getItemImage($item) 53 | { 54 | return static::getIcon(); 55 | } 56 | 57 | public static function getIcon() 58 | { 59 | return Icon::get('star')->asString(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /widgets/Messages.php: -------------------------------------------------------------------------------- 1 | getEntries(); 38 | foreach ($entries as $index => $entry) { 39 | try { 40 | $nextEntry = $entries[$index + 1] ?? null; 41 | $result .= ConversationEntry::widget([ 42 | 'entry' => $entry, 43 | 'prevEntry' => $prevEntry, 44 | 'nextEntry' => $nextEntry, 45 | 'showDateBadge' => $this->showDateBadge, 46 | ]); 47 | $prevEntry = $entry; 48 | } catch (\Throwable $e) { 49 | Yii::error($e); 50 | } 51 | } 52 | 53 | return $result; 54 | } 55 | 56 | /** 57 | * @return MessageEntry[] 58 | */ 59 | private function getEntries() 60 | { 61 | if ($this->entries) { 62 | return $this->entries; 63 | } 64 | 65 | return $this->message->getEntryPage($this->from); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /tests/codeception/api/UserCest.php: -------------------------------------------------------------------------------- 1 | isRestModuleEnabled()) { 13 | return; 14 | } 15 | 16 | $I->wantTo('see recipients of the conversation by id'); 17 | $I->amAdmin(); 18 | $I->sendGet('mail/3/users'); 19 | $I->seeUserDefinitions(['Admin', 'User1', 'User2']); 20 | } 21 | 22 | public function testAddRecipient(ApiTester $I) 23 | { 24 | if (!$this->isRestModuleEnabled()) { 25 | return; 26 | } 27 | 28 | $I->wantTo('add recipient to the conversation'); 29 | $I->amUser2(); 30 | $I->sendPost('mail/3/user/4'); 31 | $I->seeUserDefinitions(['Admin', 'User1', 'User2', 'User3']); 32 | 33 | $I->sendPost('mail/3/user/4'); 34 | $I->seeBadResponseContainsJson(['message' => 'User is already a participant of the conversation.']); 35 | } 36 | 37 | public function testRemoveRecipient(ApiTester $I) 38 | { 39 | if (!$this->isRestModuleEnabled()) { 40 | return; 41 | } 42 | 43 | $I->wantTo('remove recipient from the conversation'); 44 | $I->amUser2(); 45 | $I->sendDelete('mail/3/user/2'); 46 | $I->seeUserDefinitions(['Admin', 'User2']); 47 | 48 | $I->sendDelete('mail/3/user/2'); 49 | $I->seeBadResponseContainsJson(['message' => 'User is not a participant of the conversation.']); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /widgets/ConversationDateBadge.php: -------------------------------------------------------------------------------- 1 | 'conversation-entry-badge conversation-date-badge']; 22 | public string $format = 'long'; 23 | 24 | /** 25 | * @inheritdoc 26 | */ 27 | public function run() 28 | { 29 | return Html::tag('div', Html::tag('span', $this->renderDate()), $this->options); 30 | } 31 | 32 | protected function renderDate(): string 33 | { 34 | if ($this->isDate('today')) { 35 | return Yii::t('MailModule.base', 'Today'); 36 | } 37 | 38 | if ($this->isDate('yesterday')) { 39 | return Yii::t('MailModule.base', 'Yesterday'); 40 | } 41 | 42 | return $this->getFormattedEntryDate(); 43 | } 44 | 45 | private function getFormattedEntryDate(): string 46 | { 47 | return Yii::$app->formatter->asDate($this->entry->created_at, $this->format); 48 | } 49 | 50 | private function isDate(string $date): bool 51 | { 52 | $datetime = new DateTime($date, new DateTimeZone(Yii::$app->formatter->timeZone)); 53 | 54 | return $this->getFormattedEntryDate() === Yii::$app->formatter->asDate($datetime, $this->format); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /widgets/ConversationInbox.php: -------------------------------------------------------------------------------- 1 | result = $this->filter->getPage(); 46 | } 47 | 48 | /** 49 | * @inheritDoc 50 | */ 51 | public function run() 52 | { 53 | return $this->render('inbox', [ 54 | 'options' => $this->getOptions(), 55 | 'userMessages' => $this->result, 56 | ]); 57 | } 58 | 59 | public function getData() 60 | { 61 | return [ 62 | 'widget-reload-url' => Url::toUpdateInbox(), 63 | 'load-more-url' => Url::toInboxLoadMore(), 64 | 'update-entries-url' => Url::toInboxUpdateEntries(), 65 | 'is-last' => $this->filter->wasLastPage(), 66 | ]; 67 | } 68 | 69 | public function getAttributes() 70 | { 71 | return [ 72 | 'class' => 'hh-list', 73 | ]; 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /widgets/views/notificationInbox.php: -------------------------------------------------------------------------------- 1 | user->can(StartConversation::class); 14 | 15 | ?> 16 |
17 | 18 | id('badge-messages')->cssClass('d-none') ?> 19 | 44 |
45 | -------------------------------------------------------------------------------- /widgets/views/inboxMessagePreview.php: -------------------------------------------------------------------------------- 1 | 21 | 22 | 47 | 48 | -------------------------------------------------------------------------------- /tests/codeception/fixtures/data/message_entry.php: -------------------------------------------------------------------------------- 1 | 1, 'message_id' => 1, 'user_id' => 1, 'content' => 'First Message entry text 1.', 'created_by' => 1, 'created_at' => '2021-03-15 14:38:49'], 22 | ['id' => 2, 'message_id' => 2, 'user_id' => 1, 'content' => 'Second Message entry text 1.', 'created_by' => 1, 'created_at' => '2021-03-15 14:38:49'], 23 | ['id' => 3, 'message_id' => 2, 'user_id' => 2, 'content' => 'Second Message entry text 2.', 'created_by' => 2, 'created_at' => '2021-03-15 14:38:49'], 24 | ['id' => 4, 'message_id' => 3, 'user_id' => 1, 'content' => 'Third Message entry text 1.', 'created_by' => 1, 'created_at' => '2021-03-15 14:38:49'], 25 | ['id' => 5, 'message_id' => 3, 'user_id' => 2, 'content' => 'Third Message entry text 2.', 'created_by' => 2, 'created_at' => '2021-03-15 14:38:49'], 26 | ['id' => 6, 'message_id' => 3, 'user_id' => 3, 'content' => 'Third Message entry text 3.', 'created_by' => 3, 'created_at' => '2021-03-15 14:38:49'], 27 | ]; 28 | -------------------------------------------------------------------------------- /tests/codeception/api/TagCest.php: -------------------------------------------------------------------------------- 1 | isRestModuleEnabled()) { 13 | return; 14 | } 15 | 16 | $I->wantTo('see tags of the conversation by id'); 17 | $I->amAdmin(); 18 | $I->seePaginationGetResponse('mail/1/tags', [ 19 | ['id' => 1, 'name' => 'Tag admin 1', 'sort_order' => 10], 20 | ['id' => 2, 'name' => 'Tag admin 2', 'sort_order' => 20], 21 | ['id' => 3, 'name' => 'Tag admin 3', 'sort_order' => 30], 22 | ]); 23 | } 24 | 25 | public function testUpdateTags(ApiTester $I) 26 | { 27 | if (!$this->isRestModuleEnabled()) { 28 | return; 29 | } 30 | 31 | $I->wantTo('update tags'); 32 | $I->amUser1(); 33 | $I->seePaginationPutResponse( 34 | 'mail/2/tags', 35 | ['tags' => ['User1 tag 1', 'User1 tag 2', 'User1 tag 3']], 36 | [ 37 | ['id' => 7, 'name' => 'User1 tag 1'], 38 | ['id' => 8, 'name' => 'User1 tag 2'], 39 | ['id' => 9, 'name' => 'User1 tag 3'], 40 | ], 41 | ); 42 | } 43 | 44 | public function testCannotUpdateTags(ApiTester $I) 45 | { 46 | if (!$this->isRestModuleEnabled()) { 47 | return; 48 | } 49 | 50 | $I->wantTo('cannot update tags'); 51 | $I->amUser3(); 52 | $I->sendPut('mail/2/tags', ['tags' => ['User1 tag 1', 'User1 tag 2', 'User1 tag 3']]); 53 | $I->seeForbiddenResponseContainsJson([ 54 | 'message' => 'You must be a participant of the conversation.', 55 | ]); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /widgets/NewMessageButton.php: -------------------------------------------------------------------------------- 1 | getLabel())->load(Url::toCreateConversation($this->guid))->id($this->id); 53 | 54 | if ($this->icon) { 55 | $button->icon($this->icon); 56 | } 57 | 58 | if ($this->right) { 59 | $button->right(); 60 | } 61 | 62 | if ($this->cssClass) { 63 | $button->cssClass($this->cssClass); 64 | } 65 | 66 | match ($this->size) { 67 | 'sm', 'small' => $button->sm(), 68 | 'lg', 'large' => $button->lg(), 69 | default => $button, 70 | }; 71 | 72 | return $button; 73 | } 74 | 75 | public function getLabel() 76 | { 77 | if ($this->label !== null) { 78 | return $this->label; 79 | } 80 | 81 | return ($this->guid) 82 | ? Yii::t('MailModule.base', 'Send message') 83 | : Yii::t('MailModule.base', 'Message'); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /widgets/views/conversationEntry.php: -------------------------------------------------------------------------------- 1 | 23 | 24 | $entry]) ?> 25 | 26 | 27 | 28 | 29 |
30 | 31 | $entry]) ?> 32 | 33 | 34 | 35 | $entry->user, 37 | 'width' => 30, 38 | ]) ?> 39 | 40 | 41 | 42 |
43 |
44 | 45 |
user->displayName) ?>
47 | 48 | content) ?> 49 | $entry]) ?> 50 |
51 | $entry]) ?> 52 |
53 |
54 | 55 | 56 | -------------------------------------------------------------------------------- /search/SearchRecord.php: -------------------------------------------------------------------------------- 1 | preview = new InboxMessagePreview(['userMessage' => $userMessage]); 29 | } 30 | 31 | /** 32 | * @inheritdoc 33 | */ 34 | public function getImage(): string 35 | { 36 | $lastParticipant = $this->preview->lastParticipant(); 37 | 38 | if ($lastParticipant instanceof User) { 39 | return Image::widget([ 40 | 'user' => $lastParticipant, 41 | 'width' => 36, 42 | 'link' => false, 43 | 'hideOnlineStatus' => true, 44 | ]); 45 | } 46 | 47 | return Icon::get('envelope'); 48 | } 49 | 50 | /** 51 | * @inheritdoc 52 | */ 53 | public function getTitle(): string 54 | { 55 | return $this->preview->getMessage()->title; 56 | } 57 | 58 | /** 59 | * @inheritdoc 60 | */ 61 | public function getDescription(): string 62 | { 63 | return $this->preview->getMessagePreview(); 64 | } 65 | 66 | /** 67 | * @inheritdoc 68 | */ 69 | public function getUrl(): string 70 | { 71 | return Url::toMessenger($this->preview->getMessage()); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /helpers/RestDefinitions.php: -------------------------------------------------------------------------------- 1 | $message->id, 25 | 'title' => $message->title, 26 | 'created_at' => $message->created_at, 27 | 'created_by' => $message->created_by, 28 | 'updated_at' => $message->updated_at, 29 | 'updated_by' => $message->updated_by, 30 | ]; 31 | } 32 | 33 | public static function getMessageEntry(MessageEntry $entry) 34 | { 35 | return [ 36 | 'id' => $entry->id, 37 | 'user_id' => $entry->user_id, 38 | 'content' => $entry->content, 39 | 'type' => $entry->type, 40 | 'created_at' => $entry->created_at, 41 | 'created_by' => $entry->created_by, 42 | 'updated_at' => $entry->updated_at, 43 | 'updated_by' => $entry->updated_by, 44 | ]; 45 | } 46 | 47 | public static function getMessageUsers(Message $message) 48 | { 49 | $messageUsers = []; 50 | foreach ($message->getUsers()->all() as $messageUser) { 51 | $messageUsers[] = UserDefinitions::getUser($messageUser); 52 | } 53 | return $messageUsers; 54 | } 55 | 56 | public static function getMessageTag(MessageTag $tag) 57 | { 58 | return [ 59 | 'id' => $tag->id, 60 | 'name' => $tag->name, 61 | 'sort_order' => $tag->sort_order, 62 | 'color' => $tag->color, 63 | ]; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /models/UserMessageTag.php: -------------------------------------------------------------------------------- 1 | ['message_id', 'user_id', 'tag_id']], 32 | ]; 33 | } 34 | 35 | /** 36 | * @param UserMessage $message 37 | * @param MessageTag $userTag 38 | */ 39 | public static function create(UserMessage $message, MessageTag $userTag) 40 | { 41 | (new static(['message_id' => $message->message_id, 'user_id' => $message->user_id, 'tag_id' => $userTag->id]))->save(); 42 | } 43 | 44 | /** 45 | * @return ActiveQuery 46 | */ 47 | public function getMessage() 48 | { 49 | return $this->hasOne(Message::class, ['id' => 'message_id']); 50 | } 51 | 52 | /** 53 | * @return ActiveQuery 54 | */ 55 | public function getUser() 56 | { 57 | return $this->hasOne(User::class, ['id' => 'user_id']); 58 | } 59 | 60 | /** 61 | * @return ActiveQuery 62 | */ 63 | public function getTag() 64 | { 65 | return $this->hasOne(MessageTag::class, ['id' => 'tag_id']); 66 | } 67 | 68 | /** 69 | * @param $userId 70 | * @param UserMessage $message 71 | * @return ActiveQuery 72 | */ 73 | public static function findAllByUserMessage(UserMessage $message) 74 | { 75 | return static::find() 76 | ->where(['message_id' => $message->message_id]) 77 | ->andWhere(['user_id' => $message->user_id]); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /migrations/m131023_165507_initial.php: -------------------------------------------------------------------------------- 1 | createTable('user_message', [ 11 | 'message_id' => 'int(11) NOT NULL', 12 | 'user_id' => 'int(11) NOT NULL', 13 | 'is_originator' => 'tinyint(4) DEFAULT NULL', 14 | 'last_viewed' => 'datetime DEFAULT NULL', 15 | 'created_at' => 'datetime DEFAULT NULL', 16 | 'created_by' => 'int(11) DEFAULT NULL', 17 | 'updated_at' => 'datetime DEFAULT NULL', 18 | 'updated_by' => 'int(11) DEFAULT NULL', 19 | ], ''); 20 | 21 | $this->addPrimaryKey('pk_user_message', 'user_message', 'message_id,user_id'); 22 | 23 | 24 | $this->createTable('message', [ 25 | 'id' => 'pk', 26 | 'title' => 'varchar(255) DEFAULT NULL', 27 | 'created_at' => 'datetime DEFAULT NULL', 28 | 'created_by' => 'int(11) DEFAULT NULL', 29 | 'updated_at' => 'datetime DEFAULT NULL', 30 | 'updated_by' => 'int(11) DEFAULT NULL', 31 | ], ''); 32 | 33 | $this->createTable('message_entry', [ 34 | 'id' => 'pk', 35 | 'message_id' => 'int(11) NOT NULL', 36 | 'user_id' => 'int(11) NOT NULL', 37 | 'file_id' => 'int(11) DEFAULT NULL', 38 | 'content' => 'text NOT NULL', 39 | 'created_at' => 'datetime DEFAULT NULL', 40 | 'created_by' => 'int(11) DEFAULT NULL', 41 | 'updated_at' => 'datetime DEFAULT NULL', 42 | 'updated_by' => 'int(11) DEFAULT NULL', 43 | ], ''); 44 | } 45 | 46 | public function down() 47 | { 48 | echo "m131023_165507_initial does not support migration down.\n"; 49 | return false; 50 | } 51 | 52 | /* 53 | // Use safeUp/safeDown to do migration with transaction 54 | public function safeUp() 55 | { 56 | } 57 | 58 | public function safeDown() 59 | { 60 | } 61 | */ 62 | } 63 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 3 | grunt.initConfig({ 4 | pkg: grunt.file.readJSON('package.json'), 5 | uglify: { 6 | mail: { 7 | files: { 8 | 'resources/js/humhub.mail.messenger.bundle.min.js': ['resources/js/humhub.mail.messenger.bundle.js'], 9 | 'resources/js/humhub.mail.notification.min.js': ['resources/js/humhub.mail.notification.js'], 10 | } 11 | } 12 | }, 13 | sass: { 14 | options: { 15 | implementation: require('sass') 16 | }, 17 | dev: { 18 | files: { 19 | 'resources/css/humhub.mail.css': 'resources/css/humhub.mail.scss' 20 | } 21 | } 22 | }, 23 | cssmin: { 24 | target: { 25 | files: { 26 | 'resources/css/humhub.mail.min.css': ['resources/css/humhub.mail.css'] 27 | } 28 | } 29 | }, 30 | concat: { 31 | messenger: { 32 | src:[ 33 | 'resources/js/humhub.mail.ConversationView.js', 34 | 'resources/js/humhub.mail.ConversationViewEntry.js', 35 | 'resources/js/humhub.mail.inbox.js', 36 | 'resources/js/humhub.mail.conversation.js', 37 | ], 38 | dest: 'resources/js/humhub.mail.messenger.bundle.js' 39 | }, 40 | }, 41 | watch: { 42 | scripts: { 43 | files: ['resources/js/*.js', 'resources/css/*.scss'], 44 | tasks: ['build'], 45 | options: { 46 | spawn: false, 47 | }, 48 | }, 49 | }, 50 | }); 51 | 52 | grunt.loadNpmTasks('grunt-contrib-concat'); 53 | grunt.loadNpmTasks('grunt-contrib-uglify'); 54 | grunt.loadNpmTasks('grunt-sass'); 55 | grunt.loadNpmTasks('grunt-contrib-cssmin'); 56 | grunt.loadNpmTasks('grunt-contrib-watch'); 57 | 58 | grunt.registerTask('build', ['concat', 'uglify', 'sass', 'cssmin']); 59 | }; 60 | -------------------------------------------------------------------------------- /views/config/index.php: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 | 12 |
Messenger module configuration'); ?>
13 | 14 |
15 | 'configure-form']); ?> 16 | 17 | field($model, 'showInTopNav')->checkbox(); ?> 18 | 19 |
20 | field($model, 'userConversationRestriction')->textInput(['type' => 'number']); ?> 21 | field($model, 'userMessageRestriction')->textInput(['type' => 'number']); ?> 22 | 23 |
24 | field($model, 'newUserRestrictionEnabled')->checkbox(['id' => 'newUserCheckbox']); ?> 25 |
26 | field($model, 'newUserSinceDays')->textInput(['type' => 'number']); ?> 27 | field($model, 'newUserConversationRestriction')->textInput(['type' => 'number']); ?> 28 | field($model, 'newUserMessageRestriction')->textInput(['type' => 'number']); ?> 29 |
30 | 31 |
32 | 33 |
34 | 35 | submit() ?> 36 | 37 |
38 |
39 | 40 | 58 | -------------------------------------------------------------------------------- /tests/codeception/api/MessageCest.php: -------------------------------------------------------------------------------- 1 | isRestModuleEnabled()) { 13 | return; 14 | } 15 | 16 | $I->wantTo('see conversations of the Admin'); 17 | $I->amAdmin(); 18 | $I->seePaginationGetResponse('mail', [ 19 | ['id' => 1, 'title' => 'First message title'], 20 | ['id' => 2, 'title' => 'Second message title'], 21 | ['id' => 3, 'title' => 'Third message title'], 22 | ]); 23 | } 24 | 25 | public function testListByUser1(ApiTester $I) 26 | { 27 | if (!$this->isRestModuleEnabled()) { 28 | return; 29 | } 30 | 31 | $I->wantTo('see conversations of the User 1'); 32 | $I->amUser1(); 33 | $I->seePaginationGetResponse('mail', [ 34 | ['id' => 2, 'title' => 'Second message title'], 35 | ['id' => 3, 'title' => 'Third message title'], 36 | ]); 37 | } 38 | 39 | public function testGetConversationById(ApiTester $I) 40 | { 41 | if (!$this->isRestModuleEnabled()) { 42 | return; 43 | } 44 | 45 | $I->wantTo('see conversation by id'); 46 | $I->amUser1(); 47 | $I->sendGet('mail/2'); 48 | $I->seeSuccessResponseContainsJson(['id' => 2, 'title' => 'Second message title']); 49 | } 50 | 51 | public function testCreateConversation(ApiTester $I) 52 | { 53 | if (!$this->isRestModuleEnabled()) { 54 | return; 55 | } 56 | 57 | $I->wantTo('create conversation'); 58 | $I->amUser1(); 59 | $I->sendPost('mail', [ 60 | 'title' => ($title = 'New created conversation by User1'), 61 | 'message' => 'Sample text for the created conversation.', 62 | 'recipient' => ['01e50e0d-82cd-41fc-8b0c-552392f5839c', '01e50e0d-82cd-41fc-8b0c-552392f5839e'], 63 | ]); 64 | $I->seeSuccessResponseContainsJson(['id' => 4, 'title' => $title]); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /widgets/ConversationStateBadge.php: -------------------------------------------------------------------------------- 1 | 'conversation-entry-badge conversation-state-badge']; 25 | 26 | /** 27 | * @inheritdoc 28 | */ 29 | public function run() 30 | { 31 | $text = $this->renderInfoText(); 32 | if ($text === null) { 33 | return ''; 34 | } 35 | 36 | return Html::tag('div', Html::tag('span', Html::encode($text)), $this->getOptions()); 37 | } 38 | 39 | protected function getOptions(): array 40 | { 41 | $this->options['data-entry-id'] = $this->entry->id; 42 | 43 | return $this->options; 44 | } 45 | 46 | protected function renderInfoText(): ?string 47 | { 48 | return match ($this->entry->type) { 49 | AbstractMessageEntry::TYPE_USER_JOINED => $this->isOwn() 50 | ? Yii::t('MailModule.base', 'You joined the conversation.') 51 | : Yii::t('MailModule.base', '{username} joined the conversation.', ['username' => $this->username]), 52 | AbstractMessageEntry::TYPE_USER_LEFT => $this->isOwn() 53 | ? Yii::t('MailModule.base', 'You left the conversation.') 54 | : Yii::t('MailModule.base', '{username} left the conversation.', ['username' => $this->username]), 55 | default => null, 56 | }; 57 | } 58 | 59 | public function getUsername(): string 60 | { 61 | return $this->entry->user->displayName; 62 | } 63 | 64 | protected function isOwn(): bool 65 | { 66 | return !Yii::$app->user->isGuest 67 | && $this->entry->user->is(Yii::$app->user->getIdentity()); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /controllers/InboxController.php: -------------------------------------------------------------------------------- 1 | new InboxFilterForm(), 40 | ]); 41 | } 42 | 43 | public function actionLoadMore() 44 | { 45 | $filter = new InboxFilterForm(); 46 | $userMessages = $filter->getPage(); 47 | 48 | $result = ''; 49 | foreach ($userMessages as $userMessage) { 50 | try { 51 | $result .= InboxMessagePreview::widget(['userMessage' => $userMessage]); 52 | } catch (\Throwable $e) { 53 | Yii::error($e); 54 | } 55 | } 56 | 57 | return $this->asJson([ 58 | 'result' => $result, 59 | 'isLast' => $filter->wasLastPage(), 60 | ]); 61 | 62 | } 63 | 64 | public function actionUpdateEntries() 65 | { 66 | $filter = new InboxFilterForm(); 67 | $filter->apply(); 68 | 69 | $result = []; 70 | foreach ($filter->query->all() as $userMessage) { 71 | try { 72 | $result[$userMessage->message_id] = InboxMessagePreview::widget(['userMessage' => $userMessage]); 73 | } catch (\Throwable $e) { 74 | Yii::error($e); 75 | } 76 | } 77 | 78 | return $this->asJson([ 79 | 'result' => $result, 80 | ]); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /widgets/views/inboxFilter.php: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 | id('conversation-filter-link') 22 | ->href('#mail-filter-menu') 23 | ->icon('filter') 24 | ->cssClass('filter-toggle-link ') 25 | ->options(['data-bs-toggle' => 'collapse']) 26 | ->sm() ?> 27 | 28 |
29 |
30 | 31 | 32 | 'term', 34 | 'category' => 'term', 35 | 'view' => '@mail/widgets/views/inboxFilterTextInput', 36 | 'options' => [ 37 | 'placeholder' => Yii::t('MailModule.base', 'Search'), 38 | 'value' => $model->term, 39 | ], 40 | ]) ?> 41 | 42 |
43 | 'participants', 'category' => 'participants', 45 | 'picker' => UserPickerField::class, 46 | 'pickerOptions' => ['name' => 'participants', 'placeholder' => Yii::t('MailModule.base', 'Participants')]]) ?> 47 |
48 |
49 | 'tags', 'category' => 'tags', 51 | 'picker' => ConversationTagPicker::class, 52 | 'pickerOptions' => ['id' => 'inbox-tag-picker', 'name' => 'tags', 'placeholder' => Yii::t('MailModule.base', 'Tags'), 'placeholderMore' => Yii::t('MailModule.base', 'Tags')]]) ?> 53 | 54 | 55 | 56 | 57 |
58 | 59 |
60 | 61 | -------------------------------------------------------------------------------- /models/MessageEntry.php: -------------------------------------------------------------------------------- 1 | [self::SCENARIO_HAS_FILES]], 38 | ]); 39 | } 40 | 41 | /** 42 | * @inheritdoc 43 | */ 44 | public function canEdit(?User $user = null): bool 45 | { 46 | if ($this->type !== self::type()) { 47 | return false; 48 | } 49 | 50 | if ($user === null) { 51 | if (Yii::$app->user->isGuest) { 52 | return false; 53 | } 54 | $user = Yii::$app->user; 55 | } 56 | 57 | return $this->created_by === $user->id; 58 | } 59 | 60 | /** 61 | * @inheritdoc 62 | */ 63 | public function notify(bool $isNewConversation = false) 64 | { 65 | $messageNotification = new MessageNotification($this->message, $this); 66 | $messageNotification->isNewConversation = $isNewConversation; 67 | $messageNotification->notifyAll(); 68 | } 69 | 70 | public function isFirstToday(): bool 71 | { 72 | $today = Yii::$app->formatter->asDatetime(new DateTime('today'), 'php:Y-m-d H:i:s'); 73 | 74 | return !MessageEntry::find() 75 | ->where(['message_id' => $this->message_id]) 76 | ->andWhere(['!=', 'id', $this->id]) 77 | ->andWhere(['>=', 'created_at', $today]) 78 | ->exists(); 79 | } 80 | 81 | /** 82 | * @inheritdoc 83 | */ 84 | public function canView($user = null): bool 85 | { 86 | $message = $this->message; 87 | 88 | return $message instanceof Message && $message->isParticipant($user); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /views/mail/editEntry.php: -------------------------------------------------------------------------------- 1 | 17 | 18 | 'mail-edit', 20 | 'title' => Yii::t("MailModule.base", "Edit message entry"), 21 | 'size' => Modal::SIZE_LARGE, 22 | 'footer' => 23 | ModalButton::cancel() . ' ' . 24 | Button::save(Yii::t('base', 'Save'))->submit()->action('mail.conversation.submitEditEntry')->options(['data-entry-id' => $entry->id]) . ' ' . 25 | Button::danger(Yii::t('base', 'Delete'))->right()->options(['data-entry-id' => $entry->id]) 26 | ->action('mail.conversation.deleteEntry') 27 | ->confirm(Yii::t('MailModule.base', 'Confirm message deletion'), 28 | Yii::t('MailModule.base', 'Do you really want to delete this message?'), 29 | Yii::t('MailModule.base', 'Delete'), 30 | Yii::t('MailModule.base', 'Cancel')), 31 | ]) ?> 32 | field($entry, 'content')->widget( 33 | MailRichtextEditor::class, [ 34 | 'placeholder' => Yii::t('MailModule.base', 'Edit message...')])->label(false) ?> 35 | 36 | 'mail-edit-upload', 38 | 'model' => $entry, 39 | 'label' => Yii::t('ContentModule.base', 'Attach Files'), 40 | 'tooltip' => false, 41 | 'cssButtonClass' => 'btn-light', 42 | 'attribute' => 'files', 43 | 'progress' => '#mail-edit-progress', 44 | 'preview' => '#mail-edit-preview', 45 | 'dropZone' => '#mail-edit', 46 | 'max' => Yii::$app->getModule('content')->maxAttachedFiles, 47 | ]) ?> 48 | $uploadButton, 50 | 'handlers' => $fileHandlers, 51 | 'cssButtonClass' => 'btn-light', 52 | 'pullRight' => true, 53 | ]) ?> 54 | 'mail-edit-progress']) ?> 55 | 'mail-edit-preview', 57 | 'model' => $entry, 58 | 'edit' => true, 59 | 'options' => ['style' => 'margin-top:10px;'] 60 | ]) ?> 61 | 62 | -------------------------------------------------------------------------------- /search/SearchProvider.php: -------------------------------------------------------------------------------- 1 | route; 47 | } 48 | 49 | /** 50 | * @inheritdoc 51 | */ 52 | public function getAllResultsText(): string 53 | { 54 | return $this->getService()->hasResults() 55 | ? Yii::t('base', 'Show all results') 56 | : Yii::t('MailModule.base', 'Advanced Messages Search'); 57 | } 58 | 59 | /** 60 | * @inheritdoc 61 | */ 62 | public function getIsHiddenWhenEmpty(): bool 63 | { 64 | return true; 65 | } 66 | 67 | /** 68 | * @inheritdoc 69 | */ 70 | public function getResults(int $maxResults): array 71 | { 72 | $filter = new InboxFilterForm(['term' => $this->getKeyword()]); 73 | $filter->apply(); 74 | $totalCount = $filter->query->count(); 75 | $resultsQuery = $filter->query->limit($maxResults); 76 | 77 | $results = []; 78 | foreach ($resultsQuery->all() as $userMessage) { 79 | $results[] = new SearchRecord($userMessage); 80 | } 81 | 82 | return [ 83 | 'totalCount' => $totalCount, 84 | 'results' => $results, 85 | ]; 86 | } 87 | 88 | /** 89 | * @inheritdoc 90 | */ 91 | public function getService(): MetaSearchService 92 | { 93 | if ($this->service === null) { 94 | $this->service = new MetaSearchService($this); 95 | } 96 | 97 | return $this->service; 98 | } 99 | 100 | /** 101 | * @inheritdoc 102 | */ 103 | public function getKeyword(): ?string 104 | { 105 | return $this->keyword; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /views/mail/create.php: -------------------------------------------------------------------------------- 1 | 19 | 20 | 'mail-create', 22 | 'title' => Yii::t('MailModule.base', 'New message'), 23 | 'closable' => false, 24 | 'form' => ['enableClientValidation' => false, 'acknowledge' => true], 25 | 'footer' => ModalButton::cancel() . ' ' . ModalButton::save(Yii::t('MailModule.base', 'Send'))->submit(Url::toCreateConversation()), 26 | ]) ?> 27 | field($model, 'recipient')->widget(UserPickerField::class, 28 | [ 29 | 'url' => Url::toSearchNewParticipants(), 30 | 'placeholder' => Yii::t('MailModule.base', 'Add recipients'), 31 | ] 32 | )->label(false) ?> 33 | 34 | field($model, 'title')->textInput(['placeholder' => Yii::t('MailModule.base', 'Subject')])->label(false) ?> 35 | 36 | field($model, 'message')->widget(MailRichtextEditor::class)->label(false) ?> 37 | 38 | 'mail-upload', 40 | 'model' => $model, 41 | 'label' => Yii::t('ContentModule.base', 'Attach Files'), 42 | 'tooltip' => false, 43 | 'cssButtonClass' => 'btn-light', 44 | 'attribute' => 'files', 45 | 'progress' => '#mail-progress', 46 | 'preview' => '#mail-preview', 47 | 'dropZone' => '#mail-create', 48 | 'max' => Yii::$app->getModule('content')->maxAttachedFiles, 49 | ]) ?> 50 | $uploadButton, 52 | 'handlers' => $fileHandlers, 53 | 'cssButtonClass' => 'btn-light', 54 | 'pullRight' => true, 55 | ]) ?> 56 | 'mail-progress']) ?> 57 | 'mail-preview', 59 | 'model' => $model, 60 | 'edit' => true, 61 | 'options' => ['style' => 'margin-top:10px;'], 62 | ]) ?> 63 | 64 | field($model, 'tags')->widget(ConversationTagPicker::class, ['addOptions' => true]) */ ?> 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /controllers/rest/UserController.php: -------------------------------------------------------------------------------- 1 | getUser($userId); 47 | 48 | if ($message->isParticipant($user)) { 49 | return $this->returnError(400, 'User is already a participant of the conversation.'); 50 | } 51 | 52 | if ($message->addRecepient($user)) { 53 | return $this->actionIndex($messageId); 54 | } 55 | 56 | Yii::error('Could not add a participant into conversation.', 'api'); 57 | return $this->returnError(500, 'Internal error while add a participant into conversation!'); 58 | } 59 | 60 | /** 61 | * Leave a participant from conversation 62 | * 63 | * @param $messageId 64 | * @param $userId 65 | * @return array 66 | * @throws HttpException 67 | */ 68 | public function actionLeave($messageId, $userId) 69 | { 70 | $message = MessageController::getMessage($messageId); 71 | $user = $this->getUser($userId); 72 | 73 | if (!$message->isParticipant($user)) { 74 | return $this->returnError(400, 'User is not a participant of the conversation.'); 75 | } 76 | 77 | $message->leave($userId); 78 | 79 | return $this->actionIndex($messageId); 80 | } 81 | 82 | /** 83 | * Get user by id 84 | * 85 | * @param $id 86 | * @return User 87 | * @throws HttpException 88 | */ 89 | protected function getUser($id) 90 | { 91 | $user = User::findOne(['id' => $id]); 92 | if ($user === null) { 93 | throw new HttpException(404, 'User not found!'); 94 | } 95 | return $user; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /widgets/ParticipantUserList.php: -------------------------------------------------------------------------------- 1 | ['d-sm-none', 'd-none d-sm-inline'], 25 | 6 => ['d-none d-sm-inline d-lg-none', 'd-sm-none d-lg-inline'], 26 | 8 => ['d-none d-lg-inline', ''], 27 | ]; 28 | 29 | public function run() 30 | { 31 | $userList = $this->renderUserList(); 32 | if ($userList === '') { 33 | return ''; 34 | } 35 | 36 | return Link::asLink($userList)->action('ui.modal.load', Url::toConversationUserList($this->message)); 37 | } 38 | 39 | private function renderUserList(): string 40 | { 41 | $maxLimit = max(array_keys($this->limits)); 42 | $users = $this->message->getUsers()->limit($maxLimit)->all(); 43 | $userTotalCount = $this->message->getUsers()->count(); 44 | 45 | $usernames = ''; 46 | foreach ($users as $u => $user) { 47 | $usernames .= $this->renderUserName($user, $u, $userTotalCount); 48 | } 49 | 50 | return $usernames . $this->renderOtherCount($userTotalCount); 51 | } 52 | 53 | private function getUserNameClass(int $itemIndex): string 54 | { 55 | $classes = []; 56 | foreach ($this->limits as $limit => $class) { 57 | if ($itemIndex >= $limit) { 58 | $classes[] = $class[1]; 59 | } 60 | } 61 | return implode(' ', $classes); 62 | } 63 | 64 | private function renderUserName(User $user, int $itemIndex, int $count): string 65 | { 66 | $text = Html::encode($user->displayName) . ($itemIndex < $count - 1 ? ', ' : ''); 67 | $class = $this->getUserNameClass($itemIndex); 68 | 69 | return $class === '' ? $text : Html::tag('span', $text, ['class' => $class]); 70 | } 71 | 72 | private function renderOtherCount(int $count): string 73 | { 74 | $result = ''; 75 | foreach ($this->limits as $limit => $class) { 76 | $otherCount = $count - $limit; 77 | if ($otherCount > 0) { 78 | $otherCountText = '+' . Yii::t('MailModule.base', '{n,plural,=1{# other} other{# others}}', ['n' => $otherCount]); 79 | $result .= Html::tag('span', $otherCountText, ['class' => $class[0]]); 80 | } 81 | } 82 | 83 | return $result; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /controllers/rest/TagController.php: -------------------------------------------------------------------------------- 1 | innerJoin(UserMessageTag::tableName(), 'tag_id = id') 39 | ->where(['message_id' => $messageId]) 40 | ->andWhere([MessageTag::tableName() . '.user_id' => Yii::$app->user->id]); 41 | 42 | $pagination = $this->handlePagination($tagsQuery); 43 | foreach ($tagsQuery->all() as $tag) { 44 | $results[] = RestDefinitions::getMessageTag($tag); 45 | } 46 | return $this->returnPagination($tagsQuery, $pagination, $results); 47 | } 48 | 49 | /** 50 | * Update tags for the conversation 51 | * 52 | * @param $messageId 53 | * @return array 54 | * @throws HttpException 55 | */ 56 | public function actionUpdate($messageId) 57 | { 58 | $message = MessageController::getMessage($messageId); 59 | 60 | $conversationTagsForm = new ConversationTagsForm(['message' => $message]); 61 | 62 | $passedTags = Yii::$app->request->getBodyParam('tags', []); 63 | $updatedTags = []; 64 | foreach ($conversationTagsForm->tags as $conversationTag) { 65 | $tagIndex = array_search($conversationTag->name, $passedTags); 66 | if ($tagIndex !== false) { 67 | $updatedTags[] = $conversationTag->id; 68 | unset($passedTags[$tagIndex]); 69 | } 70 | } 71 | foreach ($passedTags as $passedTag) { 72 | $updatedTags[] = '_add:' . $passedTag; 73 | } 74 | 75 | $conversationTagsForm->load(['ConversationTagsForm' => ['tags' => $updatedTags]]); 76 | 77 | if ($conversationTagsForm->save()) { 78 | return $this->actionIndex($messageId); 79 | } 80 | 81 | if ($conversationTagsForm->hasErrors()) { 82 | return $this->returnError(400, 'Validation failed', $conversationTagsForm->getErrors()); 83 | } 84 | 85 | Yii::error('Could not create validated entry for the conversation.', 'api'); 86 | return $this->returnError(500, 'Internal error while update tags of the conversation!'); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /widgets/views/conversationSettingsMenu.php: -------------------------------------------------------------------------------- 1 | Confirm leaving conversation'); 17 | $leaveConfirmText = Yii::t('MailModule.base', 'Do you really want to leave this conversation?'); 18 | $leaveConfirmButtonText = Yii::t('MailModule.base', 'Leave'); 19 | } else { 20 | $leaveLinkText = Yii::t('MailModule.base', 'Delete conversation'); 21 | $leaveConfirmTitle = Yii::t('MailModule.base', 'Confirm deleting conversation'); 22 | $leaveConfirmText = Yii::t('MailModule.base', 'Do you really want to delete this conversation?'); 23 | $leaveConfirmButtonText = Yii::t('MailModule.base', 'Delete'); 24 | } 25 | ?> 26 | 77 | -------------------------------------------------------------------------------- /models/forms/ReplyForm.php: -------------------------------------------------------------------------------- 1 | [self::SCENARIO_HAS_FILES]], 44 | ['message', 'validateRecipients'], 45 | ]; 46 | } 47 | 48 | public function validateRecipients($attribute) 49 | { 50 | if ($this->model->isBlocked()) { 51 | $this->addError($attribute, Yii::t('MailModule.base', 'You are not allowed to reply to users {userNames}!', [ 52 | 'userNames' => implode(', ', $this->model->getBlockerNames()), 53 | ])); 54 | } 55 | } 56 | 57 | /** 58 | * Declares customized attribute labels. 59 | * If not declared here, an attribute would have a label that is 60 | * the same as its name with the first letter in upper case. 61 | */ 62 | public function attributeLabels() 63 | { 64 | return [ 65 | 'message' => Yii::t('MailModule.base', 'Message'), 66 | ]; 67 | } 68 | 69 | public function getUrl() 70 | { 71 | return Url::toReply($this->model); 72 | } 73 | 74 | public function save() 75 | { 76 | if (!$this->validate()) { 77 | return false; 78 | } 79 | 80 | $this->reply = new MessageEntry([ 81 | 'message_id' => $this->model->id, 82 | 'user_id' => Yii::$app->user->id, 83 | 'content' => $this->message, 84 | ]); 85 | if ($this->scenario === self::SCENARIO_HAS_FILES) { 86 | $this->reply->scenario = MessageEntry::SCENARIO_HAS_FILES; 87 | } 88 | 89 | if ($this->reply->save()) { 90 | $this->reply->refresh(); // Update created_by date, otherwise db expression is set... 91 | $this->reply->fileManager->attach(Yii::$app->request->post('fileList')); 92 | $this->reply->notify(); 93 | 94 | // Update last viewed date to avoid marking the conversation as unread 95 | $userMessage = $this->model->getUserMessage($this->reply->user_id); 96 | if ($userMessage) { 97 | $userMessage->last_viewed = date('Y-m-d G:i:s'); 98 | $userMessage->save(); 99 | } 100 | 101 | return true; 102 | } 103 | 104 | return false; 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /controllers/rest/MessageController.php: -------------------------------------------------------------------------------- 1 | innerJoin('user_message', 'message_id = id') 35 | ->where(['user_id' => Yii::$app->user->id]); 36 | 37 | $pagination = $this->handlePagination($messagesQuery); 38 | foreach ($messagesQuery->all() as $message) { 39 | $results[] = RestDefinitions::getMessage($message); 40 | } 41 | return $this->returnPagination($messagesQuery, $pagination, $results); 42 | } 43 | 44 | /** 45 | * Get a mail conversation by id 46 | * 47 | * @param $id 48 | * @return array 49 | * @throws HttpException 50 | */ 51 | public function actionView($id) 52 | { 53 | $message = static::getMessage($id); 54 | return RestDefinitions::getMessage($message); 55 | } 56 | 57 | /** 58 | * Create a mail conversation 59 | * 60 | * @return array 61 | * @throws \Throwable 62 | */ 63 | public function actionCreate() 64 | { 65 | if (!Yii::$app->user->isAdmin() && !Yii::$app->user->getPermissionManager()->can(StartConversation::class)) { 66 | return $this->returnError(403, 'You cannot create conversations!'); 67 | } 68 | 69 | $message = new CreateMessage(); 70 | $message->load(['CreateMessage' => Yii::$app->request->post()]); 71 | 72 | if ($message->save()) { 73 | return $this->actionView($message->messageInstance->id); 74 | } 75 | 76 | if ($message->hasErrors()) { 77 | return $this->returnError(400, 'Validation failed', $message->getErrors()); 78 | } 79 | 80 | Yii::error('Could not create validated conversation.', 'api'); 81 | return $this->returnError(500, 'Internal error while save conversation!'); 82 | } 83 | 84 | /** 85 | * Get conversation by id 86 | * 87 | * @param $id 88 | * @return Message 89 | * @throws HttpException 90 | */ 91 | public static function getMessage($id) 92 | { 93 | $message = Message::findOne(['id' => $id]); 94 | if ($message === null) { 95 | throw new HttpException(404, 'Message not found!'); 96 | } 97 | 98 | if (!$message->isParticipant(Yii::$app->user)) { 99 | throw new ForbiddenHttpException('You must be a participant of the conversation.'); 100 | } 101 | 102 | return $message; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Module.php: -------------------------------------------------------------------------------- 1 | 6 25 | */ 26 | public $inboxInitPageSize = 30; 27 | 28 | /** 29 | * @var int Defines the page size when loading more conversation page entries (autoscroller) 30 | */ 31 | public $inboxUpdatePageSize = 5; 32 | 33 | /** 34 | * @var int Defines the initial message amount loaded for a conversation 35 | */ 36 | public $conversationInitPageSize = 50; 37 | 38 | /** 39 | * @var int Defines the amount of messages loaded when loading more messages 40 | */ 41 | public $conversationUpdatePageSize = 50; 42 | 43 | /** 44 | * @inheritdoc 45 | */ 46 | public function init() 47 | { 48 | parent::init(); 49 | 50 | if (Yii::$app instanceof ConsoleApplication) { 51 | // Prevents the Yii HelpCommand from crawling all web controllers and possibly throwing errors at REST endpoints if the REST module is not available. 52 | $this->controllerNamespace = 'mail/commands'; 53 | } 54 | } 55 | 56 | /** 57 | * @return static 58 | */ 59 | public static function getModuleInstance() 60 | { 61 | return Yii::$app->getModule('mail'); 62 | } 63 | 64 | /** 65 | * @inheritdoc 66 | */ 67 | public function getConfigUrl() 68 | { 69 | return Url::toConfig(); 70 | } 71 | 72 | /** 73 | * @inheritdoc 74 | */ 75 | public function getPermissions($contentContainer = null) 76 | { 77 | if (!$contentContainer) { 78 | return [ 79 | new StartConversation(), 80 | ]; 81 | } elseif ($contentContainer instanceof User) { 82 | return [ 83 | new SendMail(), 84 | ]; 85 | } 86 | 87 | return []; 88 | } 89 | 90 | public function getNotifications() 91 | { 92 | return [ 93 | MailNotification::class, 94 | ConversationNotification::class, 95 | ]; 96 | } 97 | 98 | /** 99 | * Determines showInTopNav is enabled or not 100 | * 101 | * @return bool is showInTopNav enabled 102 | */ 103 | public function hideInTopNav() 104 | { 105 | return !$this->settings->get('showInTopNav', false); 106 | } 107 | 108 | /** 109 | * @inheritdoc 110 | */ 111 | public function disable() 112 | { 113 | foreach (MessageEntry::find()->each() as $messageEntry) { 114 | $messageEntry->delete(); 115 | } 116 | 117 | parent::disable(); 118 | } 119 | 120 | } 121 | -------------------------------------------------------------------------------- /tests/codeception/unit/UserMessageTagTest.php: -------------------------------------------------------------------------------- 1 | becomeUser('User1'); 15 | $user2 = User::findOne(['id' => 3]); 16 | 17 | $message = new CreateMessage([ 18 | 'message' => 'Hey!', 19 | 'title' => 'Test Conversation', 20 | 'recipient' => [$user2->guid], 21 | 'tags' => $tags, 22 | ]); 23 | 24 | $this->assertTrue($message->save()); 25 | 26 | return $message; 27 | } 28 | 29 | public function testSingleTagIsCreatedOnMessageCreation() 30 | { 31 | $message = $this->createMessage(['_add:TestTag']); 32 | 33 | $this->assertCount(7, MessageTag::find()->all()); 34 | $this->assertCount(7, UserMessageTag::find()->all()); 35 | 36 | /** @var MessageTag[] $tag */ 37 | $tags = MessageTag::findByMessage(Yii::$app->user->id, $message->messageInstance)->all(); 38 | $this->assertNotNull($tags); 39 | $this->assertCount(1, $tags); 40 | $this->assertEquals(Yii::$app->user->id, $tags[0]->user_id); 41 | $this->assertEquals('TestTag', $tags[0]->name); 42 | } 43 | 44 | public function testMultipleTagIsCreatedOnMessageCreation() 45 | { 46 | $message = $this->createMessage(['_add:TestTag', '_add:TestTag2']); 47 | 48 | $this->assertCount(8, MessageTag::find()->all()); 49 | $this->assertCount(8, UserMessageTag::find()->all()); 50 | 51 | /** @var MessageTag[] $tag */ 52 | $tags = MessageTag::findByMessage(Yii::$app->user->id, $message->messageInstance)->all(); 53 | $this->assertNotNull($tags); 54 | $this->assertCount(2, $tags); 55 | $this->assertEquals(Yii::$app->user->id, $tags[0]->user_id); 56 | $this->assertEquals(Yii::$app->user->id, $tags[1]->user_id); 57 | $this->assertEquals('TestTag', $tags[0]->name); 58 | $this->assertEquals('TestTag2', $tags[1]->name); 59 | } 60 | 61 | public function testDuplicateTagIsAttachedOnlyOnce() 62 | { 63 | $message = $this->createMessage(['_add:TestTag', '_add:TestTag']); 64 | 65 | $this->assertCount(7, MessageTag::find()->all()); 66 | $this->assertCount(7, UserMessageTag::find()->all()); 67 | 68 | /** @var MessageTag[] $tag */ 69 | $tags = MessageTag::findByMessage(Yii::$app->user->id, $message->messageInstance)->all(); 70 | $this->assertNotNull($tags); 71 | $this->assertCount(1, $tags); 72 | $this->assertEquals(Yii::$app->user->id, $tags[0]->user_id); 73 | $this->assertEquals('TestTag', $tags[0]->name); 74 | } 75 | 76 | public function testMissingTagsAreDeletedOnAttach() 77 | { 78 | $message = $this->createMessage(['_add:TestTag', '_add:TestTag2']); 79 | 80 | $tags = MessageTag::findByMessage(Yii::$app->user->id, $message->messageInstance)->all(); 81 | $editForm = new ConversationTagsForm(['message' => $message->messageInstance, 'tags' => [$tags[0]]]); 82 | $editForm->save(); 83 | 84 | $updatedTags = MessageTag::findByMessage(Yii::$app->user->id, $message->messageInstance)->all(); 85 | $this->assertCount(1, $updatedTags); 86 | $this->assertEquals('TestTag', $updatedTags[0]->name); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /resources/js/humhub.mail.conversation.js: -------------------------------------------------------------------------------- 1 | humhub.module('mail.conversation', function (module, require, $) { 2 | var Widget = require('ui.widget').Widget; 3 | var modal = require('ui.modal'); 4 | var client = require('client'); 5 | var event = require('event'); 6 | var mail = require('mail.notification'); 7 | 8 | var submitEditEntry = function (evt) { 9 | modal.submit(evt).then(function (response) { 10 | if (response.success) { 11 | var entry = getEntry(evt.$trigger.data('entry-id')); 12 | if (entry) { 13 | setTimeout(function () { 14 | entry.replace(response.content); 15 | }, 300) 16 | } 17 | 18 | return; 19 | } 20 | 21 | module.log.error(null, true); 22 | }).catch(function (e) { 23 | module.log.error(e, true); 24 | }); 25 | }; 26 | 27 | var deleteEntry = function (evt) { 28 | var entry = getEntry(evt.$trigger.data('entry-id')); 29 | 30 | if (!entry) { 31 | module.log.error(null, true); 32 | return; 33 | } 34 | 35 | client.post(entry.options.deleteUrl).then(function (response) { 36 | modal.global.close(); 37 | 38 | if (response.success) { 39 | setTimeout(function () { 40 | entry.remove(); 41 | }, 1000); 42 | } 43 | }).catch(function (e) { 44 | module.log.error(e, true); 45 | }); 46 | }; 47 | 48 | var getEntry = function (id) { 49 | return Widget.instance('.mail-conversation-entry[data-entry-id="' + id + '"]'); 50 | }; 51 | 52 | var getRootView = function () { 53 | return Widget.instance('#mail-conversation-root'); 54 | }; 55 | 56 | var init = function () { 57 | event.on('humhub:modules:mail:live:NewUserMessage', function (evt, events) { 58 | if(!$('#inbox').length) { 59 | return; 60 | } 61 | 62 | var root = getRootView(); 63 | var updated = false; 64 | var updatedMessages = []; 65 | events.forEach(function (event) { 66 | updatedMessages.push(event.data.message_id); 67 | if (!updated && root && root.options.messageId == event.data.message_id) { 68 | root.loadUpdate(); 69 | updated = true; 70 | root.markSeen(event.data.message_id); 71 | } 72 | }); 73 | 74 | Widget.instance('#inbox').updateEntries(updatedMessages); 75 | }).on('humhub:modules:mail:live:UserMessageDeleted', function (evt, events, update) { 76 | if(!$('#inbox').length) { 77 | return; 78 | } 79 | 80 | events.forEach(function (event) { 81 | var entry = getEntry(event.data.entry_id); 82 | if (entry) { 83 | entry.remove(); 84 | } 85 | mail.setMailMessageCount(event.data.count); 86 | }); 87 | }); 88 | }; 89 | 90 | var linkAction = function (evt) { 91 | client.post(evt).then(function (response) { 92 | if (response.redirect) { 93 | client.pjax.redirect(response.redirect); 94 | } 95 | }).catch(function (e) { 96 | module.log.error(e, true); 97 | }); 98 | }; 99 | 100 | module.export({ 101 | init, 102 | linkAction, 103 | submitEditEntry, 104 | deleteEntry, 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /resources/js/humhub.mail.notification.js: -------------------------------------------------------------------------------- 1 | humhub.module('mail.notification', function (module, require, $) { 2 | var client = require('client'); 3 | var loader = require('ui.loader'); 4 | var event = require('event'); 5 | var Widget = require('ui.widget').Widget; 6 | var currentXhr; 7 | var newMessageCount = 0; 8 | 9 | module.initOnPjaxLoad = true; 10 | 11 | var init = function (isPjax) { 12 | // open the messages menu 13 | if (!isPjax) { 14 | event.on('humhub:modules:mail:live:NewUserMessage', function (evt, events) { 15 | var evtx = events[events.length - 1]; 16 | setMailMessageCount(evtx.data.count); 17 | }).on('humhub:modules:mail:live:UserMessageDeleted', function (evt, events) { 18 | var evtx = events[events.length - 1]; 19 | setMailMessageCount(evtx.data.count); 20 | }); 21 | 22 | 23 | $('#icon-messages').click(function () { 24 | if (currentXhr) { 25 | currentXhr.abort(); 26 | } 27 | 28 | const messageLoader = $('#loader_messages'); 29 | const messageList = messageLoader.parent(); 30 | 31 | // remove all entries from dropdown 32 | messageLoader.parent().find(':not(#loader_messages)').remove(); 33 | loader.set(messageLoader.removeClass('d-none')); 34 | 35 | client.get(module.config.url.list, { 36 | beforeSend: function (xhr) { 37 | currentXhr = xhr; 38 | } 39 | }).then(function (response) { 40 | currentXhr = undefined; 41 | messageList.prepend($(response.html)); 42 | messageLoader.addClass('d-none'); 43 | messageList.niceScroll({ 44 | cursorwidth: '7', 45 | cursorborder: '', 46 | cursorcolor: '#555', 47 | cursoropacitymax: '0.2', 48 | nativeparentscrolling: false, 49 | railpadding: {top: 0, right: 3, left: 0, bottom: 0} 50 | }); 51 | }); 52 | }); 53 | } 54 | 55 | updateCount(); 56 | }; 57 | 58 | var updateCount = function () { 59 | client.get(module.config.url.count).then(function (response) { 60 | setMailMessageCount(parseInt(response.newMessages)); 61 | }); 62 | }; 63 | 64 | var setMailMessageCount = function (count) { 65 | // show or hide the badge for new messages 66 | var $badge = $('#badge-messages'); 67 | if (!count || parseInt(count) === 0) { 68 | $badge.addClass('d-none'); 69 | newMessageCount = 0; 70 | } else { 71 | $badge.removeClass('d-none'); 72 | newMessageCount = count; 73 | $badge.empty(); 74 | $badge.append(count); 75 | $badge.fadeIn('fast'); 76 | } 77 | 78 | event.trigger('humhub:modules:notification:UpdateTitleNotificationCount'); 79 | }; 80 | 81 | var loadMessage = function (evt) { 82 | var root = Widget.instance('#mail-conversation-root'); 83 | if (root && typeof(root.loadMessage) === 'function') { 84 | root.loadMessage(evt); 85 | root.$.closest('.container').addClass('mail-conversation-single-message'); 86 | } else { 87 | client.redirect(evt.url); 88 | } 89 | evt.finish(); 90 | }; 91 | 92 | var getNewMessageCount = function () { 93 | return newMessageCount; 94 | }; 95 | 96 | module.export({ 97 | init: init, 98 | loadMessage: loadMessage, 99 | setMailMessageCount: setMailMessageCount, 100 | updateCount: updateCount, 101 | getNewMessageCount: getNewMessageCount, 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /controllers/TagController.php: -------------------------------------------------------------------------------- 1 | render('manage', ['model' => new AddTag()]); 40 | } 41 | 42 | public function actionAdd() 43 | { 44 | $model = new AddTag(); 45 | $model->load(Yii::$app->request->post()); 46 | if ($model->save()) { 47 | $model = new AddTag(); 48 | } 49 | return $this->render('manage', ['model' => $model]); 50 | } 51 | 52 | public function actionEdit($id) 53 | { 54 | $tag = $this->findTag($id); 55 | 56 | if ($tag->load(Yii::$app->request->post()) && $tag->save()) { 57 | return ModalClose::widget(['reload' => true]); 58 | } 59 | 60 | return $this->renderAjax('editModal', ['model' => $tag]); 61 | 62 | } 63 | 64 | /** 65 | * @param $id 66 | * @return MessageTag 67 | * @throws NotFoundHttpException 68 | */ 69 | private function findTag($id) 70 | { 71 | $tag = MessageTag::findByUser(Yii::$app->user->id)->andWhere(['id' => $id])->one(); 72 | 73 | if (!$tag) { 74 | throw new NotFoundHttpException(); 75 | } 76 | 77 | return $tag; 78 | } 79 | 80 | /** 81 | * @param $id 82 | * @return TagController|\yii\console\Response|\yii\web\Response 83 | * @throws NotFoundHttpException 84 | * @throws \Throwable 85 | * @throws \yii\db\StaleObjectException 86 | * @throws \yii\web\HttpException 87 | */ 88 | public function actionDelete($id) 89 | { 90 | $this->forcePostRequest(); 91 | $this->findTag($id)->delete(); 92 | return $this->redirect(Url::toManageTags()); 93 | } 94 | 95 | public function actionSearch($keyword) 96 | { 97 | $results = MessageTag::search(Yii::$app->user->id, $keyword); 98 | 99 | return $this->asJson(array_map(fn(MessageTag $tag) => ['id' => $tag->id, 'text' => $tag->name, 'image' => ConversationTagPicker::getIcon()], $results)); 100 | } 101 | 102 | public function actionEditConversation($messageId) 103 | { 104 | $message = Message::findOne(['id' => $messageId]); 105 | 106 | if (!$message) { 107 | throw new NotFoundHttpException(); 108 | } 109 | 110 | if (!$message->isParticipant(Yii::$app->user->getIdentity())) { 111 | throw new ForbiddenHttpException(); 112 | } 113 | 114 | $model = new ConversationTagsForm(['message' => $message]); 115 | 116 | if ($model->load(Yii::$app->request->post()) && $model->save()) { 117 | return ModalClose::widget([ 118 | 'script' => '$("#' . ConversationTags::ID . '").replaceWith(\'' . ConversationTags::widget(['message' => $message]) . '\');', 119 | ]); 120 | } 121 | 122 | return $this->renderAjax('editConversationTagsModal', ['model' => new ConversationTagsForm(['message' => $message])]); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /models/forms/InviteParticipantForm.php: -------------------------------------------------------------------------------- 1 | Yii::t('MailModule.base', 'Recipient'), 58 | ]; 59 | } 60 | 61 | /** 62 | * Form Validator which checks the recipient field 63 | * 64 | * @param type $attribute 65 | * @param type $params 66 | */ 67 | public function checkRecipient($attribute, $params) 68 | { 69 | foreach ($this->recipients as $userGuid) { 70 | $user = User::findOne(['guid' => $userGuid]); 71 | if ($user) { 72 | $name = Html::encode($user->getDisplayName()); 73 | if (Yii::$app->user->identity->is($user)) { 74 | $this->addError($attribute, Yii::t('MailModule.base', "You cannot send a email to yourself!")); 75 | } elseif ($this->message->isParticipant($user)) { 76 | $this->addError($attribute, Yii::t('MailModule.base', "User {name} is already participating!", ['name' => $name])); 77 | } elseif (!$user->can(SendMail::class) && !Yii::$app->user->isAdmin()) { 78 | $this->addError($attribute, Yii::t('MailModule.base', "You are not allowed to send user {name} is already!", ['name' => $name])); 79 | } else { 80 | $this->recipientUsers[] = $user; 81 | } 82 | } 83 | } 84 | } 85 | 86 | public function getPickerUrl() 87 | { 88 | return Url::toSearchNewParticipants($this->message); 89 | } 90 | 91 | public function getUrl() 92 | { 93 | return Url::toAddParticipant($this->message); 94 | } 95 | 96 | public function save() 97 | { 98 | if (!$this->validate()) { 99 | return false; 100 | } 101 | 102 | foreach ($this->recipientUsers as $user) { 103 | $userMessage = new UserMessage([ 104 | 'message_id' => $this->message->id, 105 | 'user_id' => $user->id, 106 | 'is_originator' => 0, 107 | ]); 108 | 109 | if ($userMessage->save()) { 110 | $this->message->refresh(); 111 | (new MessageNotification($this->message)) 112 | ->setEntrySender(Yii::$app->user->getIdentity()) 113 | ->notifyAll(); 114 | } 115 | } 116 | 117 | unset($this->message->users); 118 | return true; 119 | } 120 | 121 | /** 122 | * Returns an Array with selected recipients 123 | */ 124 | public function getRecipients() 125 | { 126 | return $this->recipients; 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /views/tag/manage.php: -------------------------------------------------------------------------------- 1 | MessageTag::findByUser(Yii::$app->user->id) 22 | ]) 23 | 24 | ?> 25 |
26 |
27 |
28 |
29 |
30 | Manage conversation tags') ?> 31 | 32 | right()->sm() ?> 33 |
34 | 35 |
36 | 37 |
38 |
39 | 40 |
41 | 42 | Url::toAddTag()]); ?> 43 |
44 |
45 | tag, 'name', ['style' => 'height:36px', 'class' => 'form-control', 'placeholder' => Yii::t('MailModule.base', 'Add Tag')]) ?> 46 | icon('fa-plus')->loader()->submit() ?> 47 |
48 | 49 | tag, 'name') ?> 50 | 51 |
52 | 53 | 54 | $dataProvider, 56 | 'options' => ['class' => 'grid-view', 'style' => 'padding-top:0'], 57 | 'tableOptions' => ['class' => 'table table-hover'], 58 | 'showHeader' => false, 59 | 'summary' => false, 60 | 'columns' => [ 61 | 'name', 62 | [ 63 | 'header' => Yii::t('base', 'Actions'), 64 | 'class' => ActionColumn::class, 65 | 'options' => ['width' => '80px'], 66 | 'contentOptions' => ['style' => 'text-align:right'], 67 | 'buttons' => [ 68 | 'update' => fn($url, $model) => 69 | /* @var $model Topic */ 70 | ModalButton::primary()->load(Url::toEditTag($model->id))->icon('fa-pencil')->sm()->loader(false), 71 | 'view' => fn() => '', 72 | 'delete' => fn($url, $model) => 73 | /* @var $model Topic */ 74 | Button::danger()->icon('fa-times')->action('client.post', Url::toDeleteTag($model->id))->confirm( 75 | Yii::t('MailModule.base', 'Confirm tag deletion'), 76 | Yii::t('MailModule.base', 'Do you really want to delete this tag?'), 77 | Yii::t('base', 'Delete'))->sm()->loader(false), 78 | ], 79 | ], 80 | ]]) ?> 81 |
82 |
83 |
84 |
85 | -------------------------------------------------------------------------------- /widgets/ConversationEntry.php: -------------------------------------------------------------------------------- 1 | entry->type === MessageEntry::type()) { 52 | return $this->runMessage(); 53 | } 54 | 55 | return $this->runState(); 56 | } 57 | 58 | public function runMessage(): string 59 | { 60 | $showUser = $this->showUser(); 61 | 62 | return $this->render('conversationEntry', [ 63 | 'entry' => $this->entry, 64 | 'contentClass' => $this->getContentClass(), 65 | 'showUser' => $showUser, 66 | 'userColor' => $showUser ? $this->getUserColor() : null, 67 | 'showDateBadge' => $this->showDateBadge(), 68 | 'options' => $this->getOptions(), 69 | 'isOwnMessage' => $this->isOwnMessage(), 70 | ]); 71 | } 72 | 73 | public function runState(): string 74 | { 75 | return $this->render('conversationState', [ 76 | 'entry' => $this->entry, 77 | 'showDateBadge' => $this->showDateBadge(), 78 | ]); 79 | } 80 | 81 | private function getContentClass(): string 82 | { 83 | $result = 'conversation-entry-content'; 84 | 85 | if ($this->isOwnMessage()) { 86 | $result .= ' own'; 87 | } 88 | 89 | return $result; 90 | } 91 | 92 | private function isOwnMessage(): bool 93 | { 94 | return $this->entry->user->is(Yii::$app->user->getIdentity()); 95 | } 96 | 97 | public function getData() 98 | { 99 | return [ 100 | 'entry-id' => $this->entry->id, 101 | 'delete-url' => Url::toDeleteMessageEntry($this->entry), 102 | ]; 103 | } 104 | 105 | public function getAttributes() 106 | { 107 | $result = [ 108 | 'class' => 'media mail-conversation-entry', 109 | ]; 110 | 111 | if ($this->isOwnMessage()) { 112 | Html::addCssClass($result, 'own'); 113 | } 114 | 115 | if ($this->isPrevEntryFromSameUser()) { 116 | Html::addCssClass($result, 'hideUserInfo'); 117 | } 118 | 119 | return $result; 120 | } 121 | 122 | private function isPrevEntryFromSameUser(): bool 123 | { 124 | return $this->prevEntry && $this->prevEntry->created_by === $this->entry->created_by; 125 | } 126 | 127 | private function showUser(): bool 128 | { 129 | return !$this->isOwnMessage(); 130 | } 131 | 132 | private function getUserColor(): string 133 | { 134 | return $this->userColors[$this->entry->created_by % count($this->userColors)]; 135 | } 136 | 137 | private function showDateBadge(): bool 138 | { 139 | if (!$this->showDateBadge) { 140 | return false; 141 | } 142 | 143 | if (!$this->prevEntry) { 144 | return true; 145 | } 146 | 147 | $previousEntryDay = Yii::$app->formatter->asDatetime($this->prevEntry->created_at, 'php:Y-m-d'); 148 | $currentEntryDay = Yii::$app->formatter->asDatetime($this->entry->created_at, 'php:Y-m-d'); 149 | 150 | return $previousEntryDay !== $currentEntryDay; 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /tests/codeception/api/EntryCest.php: -------------------------------------------------------------------------------- 1 | isRestModuleEnabled()) { 13 | return; 14 | } 15 | 16 | $I->wantTo('see entries of the conversation by id'); 17 | $I->amAdmin(); 18 | $I->seePaginationGetResponse('mail/3/entries', [ 19 | ['id' => 4, 'content' => 'Third Message entry text 1.', 'user_id' => 1], 20 | ['id' => 5, 'content' => 'Third Message entry text 2.', 'user_id' => 2], 21 | ['id' => 6, 'content' => 'Third Message entry text 3.', 'user_id' => 3], 22 | ]); 23 | } 24 | 25 | public function testGetById(ApiTester $I) 26 | { 27 | if (!$this->isRestModuleEnabled()) { 28 | return; 29 | } 30 | 31 | $I->wantTo('see entry by id'); 32 | $I->amUser1(); 33 | $I->sendGet('mail/2/entry/3'); 34 | $I->seeSuccessResponseContainsJson([ 35 | 'id' => 3, 36 | 'user_id' => 2, 37 | 'content' => 'Second Message entry text 2.', 38 | ]); 39 | } 40 | 41 | public function testCreateEntry(ApiTester $I) 42 | { 43 | if (!$this->isRestModuleEnabled()) { 44 | return; 45 | } 46 | 47 | $I->wantTo('create entry'); 48 | $I->amUser1(); 49 | $newMessage = 'New sample reply for conversation #2'; 50 | $I->sendPost('mail/2/entry', ['message' => $newMessage]); 51 | $I->seeSuccessResponseContainsJson([ 52 | 'id' => 7, 53 | 'user_id' => 2, 54 | 'content' => $newMessage, 55 | ]); 56 | } 57 | 58 | public function testCreateEntryByNotParticipant(ApiTester $I) 59 | { 60 | if (!$this->isRestModuleEnabled()) { 61 | return; 62 | } 63 | 64 | $I->wantTo('cannot create entry by not participant'); 65 | $I->amUser3(); 66 | $newMessage = 'New sample reply for conversation #2'; 67 | $I->sendPost('mail/2/entry', ['message' => $newMessage]); 68 | $I->seeForbiddenResponseContainsJson([ 69 | 'message' => 'You must be a participant of the conversation.', 70 | ]); 71 | } 72 | 73 | public function testUpdateEntry(ApiTester $I) 74 | { 75 | if (!$this->isRestModuleEnabled()) { 76 | return; 77 | } 78 | 79 | $I->wantTo('update entry by id'); 80 | $I->amAdmin(); 81 | $updatedMessage = 'Updated content of the entry #4'; 82 | $I->sendPut('mail/3/entry/4', ['content' => $updatedMessage]); 83 | $I->seeSuccessResponseContainsJson([ 84 | 'id' => 4, 85 | 'user_id' => 1, 86 | 'content' => $updatedMessage, 87 | ]); 88 | } 89 | 90 | public function testCannotUpdateEntry(ApiTester $I) 91 | { 92 | if (!$this->isRestModuleEnabled()) { 93 | return; 94 | } 95 | 96 | $I->wantTo('cannot update not own entry'); 97 | $I->amUser1(); 98 | $updatedMessage = 'Updated content of the entry #2'; 99 | $I->sendPut('mail/3/entry/4', ['content' => $updatedMessage]); 100 | $I->seeForbiddenResponseContainsJson([ 101 | 'message' => 'You cannot edit the conversation entry!', 102 | ]); 103 | } 104 | 105 | public function testDeleteEntry(ApiTester $I) 106 | { 107 | if (!$this->isRestModuleEnabled()) { 108 | return; 109 | } 110 | 111 | $I->wantTo('delete entry'); 112 | $I->amUser1(); 113 | $I->sendDelete('mail/3/entry/5'); 114 | $I->seeSuccessResponseContainsJson([ 115 | 'message' => 'Conversation entry successfully deleted!', 116 | ]); 117 | } 118 | 119 | public function testCannotDeleteEntry(ApiTester $I) 120 | { 121 | if (!$this->isRestModuleEnabled()) { 122 | return; 123 | } 124 | 125 | $I->wantTo('cannot delete not own entry'); 126 | $I->amAdmin(); 127 | $I->sendDelete('mail/3/entry/6'); 128 | $I->seeForbiddenResponseContainsJson([ 129 | 'message' => 'You cannot delete the conversation entry!', 130 | ]); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /messages/am/base.php: -------------------------------------------------------------------------------- 1 | Confirm deleting conversation' => '', 4 | 'Confirm leaving conversation' => '', 5 | 'Confirm message deletion' => '', 6 | 'Confirm tag deletion' => '', 7 | 'Edit conversation tags' => '', 8 | 'Edit tag' => '', 9 | 'Manage conversation tags' => '', 10 | 'Messenger module configuration' => '', 11 | 'New conversation' => '', 12 | 'New message' => '', 13 | 'A tag with the same name already exists.' => '', 14 | 'Add Tag' => '', 15 | 'Add participants' => '', 16 | 'Add recipients' => '', 17 | 'Add user' => '', 18 | 'Advanced Messages Search' => '', 19 | 'Allow others to send you private messages' => '', 20 | 'Allow users to start new conversations' => '', 21 | 'Cancel' => 'ይቅር', 22 | 'Confirm' => 'አረጋግጥ', 23 | 'Conversation' => '', 24 | 'Conversation tags can be used to filter conversations and are only visible to you.' => '', 25 | 'Conversations' => '', 26 | 'Created At' => '', 27 | 'Created By' => '', 28 | 'Delete' => 'አስወግድ', 29 | 'Delete conversation' => '', 30 | 'Do you really want to delete this conversation?' => '', 31 | 'Do you really want to delete this message?' => '', 32 | 'Do you really want to delete this tag?' => '', 33 | 'Do you really want to leave this conversation?' => '', 34 | 'Edit' => 'ማስተካከያ', 35 | 'Edit message entry' => '', 36 | 'Edit message...' => '', 37 | 'Filter' => 'አጣራ', 38 | 'Friday' => '', 39 | 'Here you can manage your private conversation tags.' => '', 40 | 'Is Originator' => '', 41 | 'Last Viewed' => '', 42 | 'Leave' => '', 43 | 'Leave conversation' => '', 44 | 'Leave fields blank in order to disable a restriction.' => '', 45 | 'Manage Tags' => '', 46 | 'Mark Unread' => '', 47 | 'Max messages allowed per day' => '', 48 | 'Max number of messages allowed for a new user per day' => '', 49 | 'Max number of new conversations allowed for a new user per day' => '', 50 | 'Max number of new conversations allowed for a user per day' => '', 51 | 'Message' => 'መልዕክት', 52 | 'Messages' => '', 53 | 'Monday' => '', 54 | 'My Tags' => '', 55 | 'New conversation from {senderName}' => '', 56 | 'New message from {senderName}' => '', 57 | 'Participants' => '', 58 | 'Pin' => '', 59 | 'Pinned' => 'የተሰካ', 60 | 'Receive Notifications when someone opens a new conversation.' => '', 61 | 'Receive Notifications when someone sends you a message.' => '', 62 | 'Receive private messages' => '', 63 | 'Recipient' => '', 64 | 'Reply now' => '', 65 | 'Saturday' => '', 66 | 'Search' => 'ፈልግ', 67 | 'Send' => '', 68 | 'Send message' => '', 69 | 'Seperate restrictions for new users' => '', 70 | 'Show all messages' => '', 71 | 'Show menu item in top Navigation' => '', 72 | 'Start new conversations' => '', 73 | 'Subject' => 'ርዕስ', 74 | 'Sunday' => '', 75 | 'Tags' => '', 76 | 'There are no messages yet.' => '', 77 | 'This user is already participating in this conversation.' => '', 78 | 'Thursday' => '', 79 | 'Title' => 'ርዕስ', 80 | 'Today' => '', 81 | 'Tuesday' => '', 82 | 'Unpin' => 'ንቀል', 83 | 'Until a user is member since (days)' => '', 84 | 'Updated At' => '', 85 | 'Updated By' => '', 86 | 'User' => 'ተጠቃሚ', 87 | 'User {name} is already participating!' => '', 88 | 'Wednesday' => '', 89 | 'Write a message...' => '', 90 | 'Yesterday' => '', 91 | 'You' => '', 92 | 'You are not allowed to participate in this conversation. You have been blocked by: {userNames}.' => '', 93 | 'You are not allowed to reply to users {userNames}!' => '', 94 | 'You are not allowed to send user {name} is already!' => '', 95 | 'You are not allowed to start a conversation with this user.' => '', 96 | 'You are not allowed to start a conversation with {userName}!' => '', 97 | 'You cannot send a email to yourself!' => '', 98 | 'You cannot send a message to yourself!' => '', 99 | 'You cannot send a message without recipients!' => '', 100 | 'You joined the conversation.' => '', 101 | 'You left the conversation.' => '', 102 | 'You\'ve exceeded your daily amount of new conversations.' => '', 103 | 'edited' => '', 104 | '{n,plural,=1{# other} other{# others}}' => '', 105 | '{senderName} created a new conversation {conversationTitle}' => '', 106 | '{senderName} sent you a new message in {conversationTitle}' => '', 107 | '{username} joined the conversation.' => '', 108 | '{username} left the conversation.' => '', 109 | ]; 110 | -------------------------------------------------------------------------------- /messages/ko/base.php: -------------------------------------------------------------------------------- 1 | Confirm deleting conversation' => '', 4 | 'Confirm leaving conversation' => '', 5 | 'Confirm message deletion' => '', 6 | 'Confirm tag deletion' => '', 7 | 'Edit conversation tags' => '', 8 | 'Edit tag' => '', 9 | 'Manage conversation tags' => '', 10 | 'Messenger module configuration' => '', 11 | 'New conversation' => '', 12 | 'New message' => '', 13 | 'A tag with the same name already exists.' => '', 14 | 'Add Tag' => '', 15 | 'Add participants' => '', 16 | 'Add recipients' => '', 17 | 'Add user' => '', 18 | 'Advanced Messages Search' => '', 19 | 'Allow others to send you private messages' => '', 20 | 'Allow users to start new conversations' => '', 21 | 'Cancel' => '취소', 22 | 'Confirm' => '확인', 23 | 'Conversation' => '', 24 | 'Conversation tags can be used to filter conversations and are only visible to you.' => '', 25 | 'Conversations' => '', 26 | 'Created At' => '만든 위치', 27 | 'Created By' => '만든 사람', 28 | 'Delete' => '삭제', 29 | 'Delete conversation' => '', 30 | 'Do you really want to delete this conversation?' => '', 31 | 'Do you really want to delete this message?' => '', 32 | 'Do you really want to delete this tag?' => '', 33 | 'Do you really want to leave this conversation?' => '', 34 | 'Edit' => '편집', 35 | 'Edit message entry' => '', 36 | 'Edit message...' => '', 37 | 'Filter' => '필터', 38 | 'Friday' => '', 39 | 'Here you can manage your private conversation tags.' => '', 40 | 'Is Originator' => '', 41 | 'Last Viewed' => '', 42 | 'Leave' => '', 43 | 'Leave conversation' => '', 44 | 'Leave fields blank in order to disable a restriction.' => '', 45 | 'Manage Tags' => '', 46 | 'Mark Unread' => '', 47 | 'Max messages allowed per day' => '', 48 | 'Max number of messages allowed for a new user per day' => '', 49 | 'Max number of new conversations allowed for a new user per day' => '', 50 | 'Max number of new conversations allowed for a user per day' => '', 51 | 'Message' => '', 52 | 'Messages' => '', 53 | 'Monday' => '', 54 | 'My Tags' => '', 55 | 'New conversation from {senderName}' => '', 56 | 'New message from {senderName}' => '', 57 | 'Participants' => '', 58 | 'Pin' => '', 59 | 'Pinned' => '고정', 60 | 'Receive Notifications when someone opens a new conversation.' => '', 61 | 'Receive Notifications when someone sends you a message.' => '', 62 | 'Receive private messages' => '', 63 | 'Recipient' => '', 64 | 'Reply now' => '', 65 | 'Saturday' => '', 66 | 'Search' => '검색', 67 | 'Send' => '보내기', 68 | 'Send message' => '', 69 | 'Seperate restrictions for new users' => '', 70 | 'Show all messages' => '', 71 | 'Show menu item in top Navigation' => '', 72 | 'Start new conversations' => '', 73 | 'Subject' => '', 74 | 'Sunday' => '', 75 | 'Tags' => 'ㅡㅡ', 76 | 'There are no messages yet.' => '', 77 | 'This user is already participating in this conversation.' => '', 78 | 'Thursday' => '', 79 | 'Title' => '제목', 80 | 'Today' => '', 81 | 'Tuesday' => '', 82 | 'Unpin' => '고정 해제', 83 | 'Until a user is member since (days)' => '', 84 | 'Updated At' => '업데이트 날짜', 85 | 'Updated By' => '업데이트 작성자', 86 | 'User' => '사용자', 87 | 'User {name} is already participating!' => '', 88 | 'Wednesday' => '', 89 | 'Write a message...' => '', 90 | 'Yesterday' => '', 91 | 'You' => '당신', 92 | 'You are not allowed to participate in this conversation. You have been blocked by: {userNames}.' => '', 93 | 'You are not allowed to reply to users {userNames}!' => '', 94 | 'You are not allowed to send user {name} is already!' => '', 95 | 'You are not allowed to start a conversation with this user.' => '', 96 | 'You are not allowed to start a conversation with {userName}!' => '', 97 | 'You cannot send a email to yourself!' => '', 98 | 'You cannot send a message to yourself!' => '', 99 | 'You cannot send a message without recipients!' => '', 100 | 'You joined the conversation.' => '', 101 | 'You left the conversation.' => '', 102 | 'You\'ve exceeded your daily amount of new conversations.' => '', 103 | 'edited' => '', 104 | '{n,plural,=1{# other} other{# others}}' => '', 105 | '{senderName} created a new conversation {conversationTitle}' => '', 106 | '{senderName} sent you a new message in {conversationTitle}' => '', 107 | '{username} joined the conversation.' => '', 108 | '{username} left the conversation.' => '', 109 | ]; 110 | -------------------------------------------------------------------------------- /messages/ro/base.php: -------------------------------------------------------------------------------- 1 | Confirm deleting conversation' => '', 4 | 'Confirm leaving conversation' => '', 5 | 'Confirm message deletion' => '', 6 | 'Confirm tag deletion' => '', 7 | 'Edit conversation tags' => '', 8 | 'Edit tag' => '', 9 | 'Manage conversation tags' => '', 10 | 'Messenger module configuration' => '', 11 | 'New conversation' => '', 12 | 'New message' => '', 13 | 'A tag with the same name already exists.' => '', 14 | 'Add Tag' => '', 15 | 'Add participants' => '', 16 | 'Add recipients' => '', 17 | 'Add user' => '', 18 | 'Advanced Messages Search' => '', 19 | 'Allow others to send you private messages' => '', 20 | 'Allow users to start new conversations' => '', 21 | 'Cancel' => 'Anulează', 22 | 'Confirm' => 'Confirmă', 23 | 'Conversation' => '', 24 | 'Conversation tags can be used to filter conversations and are only visible to you.' => '', 25 | 'Conversations' => '', 26 | 'Created At' => '', 27 | 'Created By' => '', 28 | 'Delete' => 'Șterge', 29 | 'Delete conversation' => '', 30 | 'Do you really want to delete this conversation?' => '', 31 | 'Do you really want to delete this message?' => '', 32 | 'Do you really want to delete this tag?' => '', 33 | 'Do you really want to leave this conversation?' => '', 34 | 'Edit' => 'Editează', 35 | 'Edit message entry' => '', 36 | 'Edit message...' => '', 37 | 'Filter' => 'Filtrează', 38 | 'Friday' => '', 39 | 'Here you can manage your private conversation tags.' => '', 40 | 'Is Originator' => '', 41 | 'Last Viewed' => '', 42 | 'Leave' => '', 43 | 'Leave conversation' => '', 44 | 'Leave fields blank in order to disable a restriction.' => '', 45 | 'Manage Tags' => '', 46 | 'Mark Unread' => '', 47 | 'Max messages allowed per day' => '', 48 | 'Max number of messages allowed for a new user per day' => '', 49 | 'Max number of new conversations allowed for a new user per day' => '', 50 | 'Max number of new conversations allowed for a user per day' => '', 51 | 'Message' => 'Mesaj', 52 | 'Messages' => '', 53 | 'Monday' => '', 54 | 'My Tags' => '', 55 | 'New conversation from {senderName}' => '', 56 | 'New message from {senderName}' => '', 57 | 'Participants' => 'Participanți', 58 | 'Pin' => '', 59 | 'Pinned' => 'Fixat', 60 | 'Receive Notifications when someone opens a new conversation.' => '', 61 | 'Receive Notifications when someone sends you a message.' => '', 62 | 'Receive private messages' => '', 63 | 'Recipient' => '', 64 | 'Reply now' => '', 65 | 'Saturday' => '', 66 | 'Search' => 'Căutare', 67 | 'Send' => 'Trimite', 68 | 'Send message' => '', 69 | 'Seperate restrictions for new users' => '', 70 | 'Show all messages' => '', 71 | 'Show menu item in top Navigation' => '', 72 | 'Start new conversations' => '', 73 | 'Subject' => '제목', 74 | 'Sunday' => '', 75 | 'Tags' => '', 76 | 'There are no messages yet.' => '', 77 | 'This user is already participating in this conversation.' => '', 78 | 'Thursday' => '', 79 | 'Title' => 'Titlul', 80 | 'Today' => '', 81 | 'Tuesday' => '', 82 | 'Unpin' => 'Anulează Anunț', 83 | 'Until a user is member since (days)' => '', 84 | 'Updated At' => '', 85 | 'Updated By' => '', 86 | 'User' => 'Utilizator', 87 | 'User {name} is already participating!' => '', 88 | 'Wednesday' => '', 89 | 'Write a message...' => '', 90 | 'Yesterday' => '', 91 | 'You' => 'Tu', 92 | 'You are not allowed to participate in this conversation. You have been blocked by: {userNames}.' => '', 93 | 'You are not allowed to reply to users {userNames}!' => '', 94 | 'You are not allowed to send user {name} is already!' => '', 95 | 'You are not allowed to start a conversation with this user.' => '', 96 | 'You are not allowed to start a conversation with {userName}!' => '', 97 | 'You cannot send a email to yourself!' => '', 98 | 'You cannot send a message to yourself!' => '', 99 | 'You cannot send a message without recipients!' => '', 100 | 'You joined the conversation.' => '', 101 | 'You left the conversation.' => '', 102 | 'You\'ve exceeded your daily amount of new conversations.' => '', 103 | 'edited' => '', 104 | '{n,plural,=1{# other} other{# others}}' => '', 105 | '{senderName} created a new conversation {conversationTitle}' => '', 106 | '{senderName} sent you a new message in {conversationTitle}' => '', 107 | '{username} joined the conversation.' => '', 108 | '{username} left the conversation.' => '', 109 | ]; 110 | -------------------------------------------------------------------------------- /models/UserMessage.php: -------------------------------------------------------------------------------- 1 | hasOne(Message::class, ['id' => 'message_id']); 60 | } 61 | 62 | public function getUser() 63 | { 64 | return $this->hasOne(User::class, ['id' => 'user_id']); 65 | } 66 | 67 | /** 68 | * @return array customized attribute labels (name=>label) 69 | */ 70 | public function attributeLabels() 71 | { 72 | return [ 73 | 'message_id' => Yii::t('MailModule.base', 'Message'), 74 | 'user_id' => Yii::t('MailModule.base', 'User'), 75 | 'is_originator' => Yii::t('MailModule.base', 'Is Originator'), 76 | 'last_viewed' => Yii::t('MailModule.base', 'Last Viewed'), 77 | 'created_at' => Yii::t('MailModule.base', 'Created At'), 78 | 'created_by' => Yii::t('MailModule.base', 'Created By'), 79 | 'updated_at' => Yii::t('MailModule.base', 'Updated At'), 80 | 'updated_by' => Yii::t('MailModule.base', 'Updated By'), 81 | ]; 82 | } 83 | 84 | /** 85 | * Returns the new message count for given User Id 86 | * 87 | * @param int $userId 88 | * @return int 89 | */ 90 | public static function getNewMessageCount($userId = null) 91 | { 92 | if ($userId === null) { 93 | $userId = Yii::$app->user->id; 94 | } 95 | 96 | if ($userId instanceof User) { 97 | $userId = $userId->id; 98 | } 99 | 100 | return static::findByUser($userId) 101 | ->andWhere("message.updated_at > user_message.last_viewed OR user_message.last_viewed IS NULL") 102 | ->andWhere(["<>", 'message.updated_by', $userId])->count(); 103 | } 104 | 105 | public static function findByUser($userId = null) 106 | { 107 | if ($userId === null) { 108 | $userId = Yii::$app->user->id; 109 | } 110 | 111 | if ($userId instanceof User) { 112 | $userId = $userId->id; 113 | } 114 | 115 | return static::find()->joinWith('message') 116 | ->where(['user_message.user_id' => $userId]) 117 | ->orderBy([ 118 | 'user_message.pinned' => SORT_DESC, 119 | 'message.updated_at' => SORT_DESC, 120 | ]); 121 | } 122 | 123 | public function isUnread(): bool 124 | { 125 | return $this->message->updated_at > $this->last_viewed; 126 | } 127 | 128 | public function afterSave($insert, $changedAttributes) 129 | { 130 | parent::afterSave($insert, $changedAttributes); 131 | 132 | if ($insert && $this->informAfterAdd) { 133 | MessageUserJoined::inform($this->message, $this->user); 134 | } 135 | } 136 | 137 | /** 138 | * @inheritdoc 139 | */ 140 | public function afterDelete() 141 | { 142 | parent::afterDelete(); 143 | MessageUserLeft::inform($this->message, $this->user); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /helpers/Url.php: -------------------------------------------------------------------------------- 1 | $userGuid] : ['/mail/mail/create']; 15 | return static::to($route); 16 | } 17 | 18 | public static function toDeleteMessageEntry(MessageEntry $entry) 19 | { 20 | return static::to(['/mail/mail/delete-entry', 'id' => $entry->id]); 21 | } 22 | 23 | public static function toLoadMessage() 24 | { 25 | return static::to(['/mail/mail/show']); 26 | } 27 | 28 | public static function toUpdateMessage() 29 | { 30 | return static::to(['/mail/mail/update']); 31 | } 32 | 33 | public static function toEditMessageEntry(MessageEntry $entry) 34 | { 35 | return static::to(['/mail/mail/edit-entry', 'id' => $entry->id]); 36 | } 37 | 38 | public static function toEditConversationTags(Message $message) 39 | { 40 | return static::to(['/mail/tag/edit-conversation', 'messageId' => $message->id]); 41 | } 42 | 43 | public static function toManageTags() 44 | { 45 | return static::to(['/mail/tag/manage']); 46 | } 47 | 48 | public static function toAddTag() 49 | { 50 | return static::to(['/mail/tag/add']); 51 | } 52 | 53 | public static function toEditTag($id) 54 | { 55 | return static::to(['/mail/tag/edit', 'id' => $id]); 56 | } 57 | 58 | public static function toDeleteTag($id) 59 | { 60 | return static::to(['/mail/tag/delete', 'id' => $id]); 61 | } 62 | 63 | public static function toUpdateInbox() 64 | { 65 | return static::to(['/mail/inbox/index']); 66 | } 67 | 68 | public static function toConversationUserList(Message $message) 69 | { 70 | return static::to(['/mail/mail/user-list', 'id' => $message->id]); 71 | } 72 | 73 | public static function toMarkUnreadConversation(Message $message) 74 | { 75 | return static::to(['/mail/mail/mark-unread', 'id' => $message->id]); 76 | } 77 | 78 | public static function toPinConversation(Message $message) 79 | { 80 | return static::to(['/mail/mail/pin', 'id' => $message->id]); 81 | } 82 | 83 | public static function toUnpinConversation(Message $message) 84 | { 85 | return static::to(['/mail/mail/unpin', 'id' => $message->id]); 86 | } 87 | 88 | public static function toLeaveConversation(Message $message) 89 | { 90 | return static::to(['/mail/mail/leave', 'id' => $message->id]); 91 | } 92 | 93 | public static function toMessenger(?Message $message = null, $scheme = false) 94 | { 95 | $route = $message ? ['/mail/mail/index', 'id' => $message->id] : ['/mail/mail/index']; 96 | return static::to($route, $scheme); 97 | } 98 | 99 | public static function toConfig() 100 | { 101 | return static::to(['/mail/config']); 102 | } 103 | 104 | public static function toMessageCountUpdate() 105 | { 106 | return static::to(['/mail/mail/get-new-message-count-json']); 107 | } 108 | 109 | public static function toNotificationList() 110 | { 111 | return static::to(['/mail/mail/notification-list']); 112 | } 113 | 114 | public static function toNotificationSeen() 115 | { 116 | return static::to(['/mail/mail/seen']); 117 | } 118 | 119 | public static function toSearchNewParticipants(?Message $message = null) 120 | { 121 | $route = $message ? ['/mail/mail/search-user', 'id' => $message->id] : ['/mail/mail/search-user']; 122 | return static::to($route); 123 | } 124 | 125 | public static function toAddParticipant(Message $message) 126 | { 127 | return static::to(['/mail/mail/add-user', 'id' => $message->id]); 128 | } 129 | 130 | public static function toReply(Message $message) 131 | { 132 | return static::to(['/mail/mail/reply', 'id' => $message->id]); 133 | } 134 | 135 | public static function toInboxLoadMore() 136 | { 137 | return static::to(['/mail/inbox/load-more']); 138 | } 139 | 140 | public static function toInboxUpdateEntries() 141 | { 142 | return static::to(['/mail/inbox/update-entries']); 143 | } 144 | 145 | public static function toLoadMoreMessages() 146 | { 147 | return static::to(['/mail/mail/load-more']); 148 | } 149 | } 150 | --------------------------------------------------------------------------------