file && $message->file->isPicture"}file{/if}" data-publishedprepared="{$message->published|prepareTime}">
-
- {if="$message->encrypted"}
-
{$c->__('message.encrypted')} lock
- {elseif="$message->retracted"}
-
{$c->__('message.retracted')} delete
- {elseif="$message->file && $message->file->isPicture"}
-
-
-
- {else}
-
{autoescape="off"}{$message->body|trim|addEmojis}{/autoescape}
- {/if}
-
+
isMine()"}class="oppose"{/if} onclick="Chat_ajaxGetMessageContext('{$message->jid}', {$message->mid}); Drawer.clear()">
+ file && $message->file->isPicture"}file{/if}"
+ data-publishedprepared="{if="$search"}{$message->published|prepareDate}{else}{$message->published|prepareTime}{/if}">
+
+ {if="$message->isMuc()"}
+
{$message->resource}
+ {/if}
+ {if="$message->encrypted"}
+
{$c->__('message.encrypted')} lock
+ {elseif="$message->retracted"}
+
{$c->__('message.retracted')} delete
+ {elseif="$message->file && $message->file->isPicture"}
+
+
-
-
-
-
-
-
-
- {if="!$message->encrypted"}
-
-
- add_reaction
-
-
-
{$c->__('message.react')}
-
-
-
-
- content_copy
-
-
-
{$c->__('chatactions.copy_text')}
-
-
- {/if}
-
-
- reply
-
-
-
{$c->__('button.reply')}
-
-
-
- {if="$message->isLast()"}
-
-
- edit
-
-
-
{$c->__('button.edit')}
-
-
- {/if}
-
- {if="$message->isMine()"}
-
-
- delete
-
-
-
{$c->__('message.retract')}
-
-
- {/if}
-
- {if="$conference && $conference->presence && $conference->presence->mucrole == 'moderator' && $conference->info && $conference->info->hasModeration()"}
-
-
-
- delete
-
-
-
{$c->__('message.retract')}
-
-
- {/if}
-
-
-
-
- {$c->__('button.close')}
-
-
+ {else}
+ {if="$search"}
+
{autoescape="off"}{$message->headline|trim|addEmojis}{/autoescape}
+ {else}
+
{autoescape="off"}{$message->body|trim|addEmojis}{/autoescape}
+ {/if}
+ {/if}
+
+
+
+
\ No newline at end of file
diff --git a/app/Widgets/ChatActions/_chatactions_message_dialog.tpl b/app/Widgets/ChatActions/_chatactions_message_dialog.tpl
new file mode 100644
index 000000000..76666daf4
--- /dev/null
+++ b/app/Widgets/ChatActions/_chatactions_message_dialog.tpl
@@ -0,0 +1,84 @@
+
+
+ {autoescape="off"}
+ {$c->prepareMessage($message)}
+ {/autoescape}
+
+
+
+
+
+ {if="!$message->encrypted"}
+
+
+ add_reaction
+
+
+
{$c->__('message.react')}
+
+
+
+
+ content_copy
+
+
+
{$c->__('chatactions.copy_text')}
+
+
+ {/if}
+
+
+ reply
+
+
+
{$c->__('button.reply')}
+
+
+
+ {if="$message->isLast()"}
+
+
+ edit
+
+
+
{$c->__('button.edit')}
+
+
+ {/if}
+
+ {if="$message->isMine()"}
+
+
+ delete
+
+
+
{$c->__('message.retract')}
+
+
+ {/if}
+
+ {if="$conference && $conference->presence && $conference->presence->mucrole == 'moderator' && $conference->info && $conference->info->hasModeration()"}
+
+
+
+ delete
+
+
+
{$c->__('message.retract')}
+
+
+ {/if}
+
+
+
+
+ {$c->__('button.close')}
+
+
diff --git a/app/Widgets/ChatActions/_chatactions_search.tpl b/app/Widgets/ChatActions/_chatactions_search.tpl
new file mode 100644
index 000000000..08fe62976
--- /dev/null
+++ b/app/Widgets/ChatActions/_chatactions_search.tpl
@@ -0,0 +1,15 @@
+
+ {autoescape="off"}
+ {$c->prepareSearchPlaceholder()}
+ {/autoescape}
+
+
diff --git a/app/Widgets/ChatActions/_chatactions_search_placeholder.tpl b/app/Widgets/ChatActions/_chatactions_search_placeholder.tpl
new file mode 100644
index 000000000..a8af1bd6f
--- /dev/null
+++ b/app/Widgets/ChatActions/_chatactions_search_placeholder.tpl
@@ -0,0 +1,5 @@
+
+ search
+
{$c->__('chatactions.search_messages')}
+ {$c->__('input.open_me_using')} Ctrl + F
+
\ No newline at end of file
diff --git a/app/Widgets/ChatActions/_chatactions_search_result.tpl b/app/Widgets/ChatActions/_chatactions_search_result.tpl
new file mode 100644
index 000000000..ed4021c40
--- /dev/null
+++ b/app/Widgets/ChatActions/_chatactions_search_result.tpl
@@ -0,0 +1,15 @@
+{if="$messages->isEmpty()"}
+
+ search_off
+
{$c->__('chatactions.search_messages')}
+ {$c->__('chatactions.search_messages_empty')}
+
+{else}
+
+ {loop="$messages"}
+ {autoescape="off"}
+ {$c->prepareMessage($value, true)}
+ {/autoescape}
+ {/loop}
+
+{/if}
\ No newline at end of file
diff --git a/app/Widgets/ChatActions/chatactions.css b/app/Widgets/ChatActions/chatactions.css
index 958fbf0e8..c32de4e4b 100644
--- a/app/Widgets/ChatActions/chatactions.css
+++ b/app/Widgets/ChatActions/chatactions.css
@@ -1,34 +1,45 @@
-#chat_actions ul#message_preview {
+ul#message_preview {
background-color: rgb(var(--movim-background));
- padding-top: 1.5rem;
+ min-height: 100%;
+ box-sizing: border-box;
+ padding: 0.75rem 0;
}
-#chat_actions ul#message_preview li div.bubble {
+ul#message_preview li {
+ padding: 0.75rem;
+}
+
+ul#message_preview li > div.bubble::after {
+ white-space: nowrap;
+}
+
+ul#message_preview li div.bubble {
max-width: calc(100% - 2rem);
pointer-events: none;
}
-#chat_actions ul#message_preview li div.bubble:not(.file) {
+ul#message_preview li div.bubble:not(.file) {
padding-right: 5rem;
margin-bottom: 0;
+ padding-bottom: 3rem;
}
-#chat_actions ul#message_preview li.oppose {
+ul#message_preview li.oppose {
flex-direction: row-reverse;
}
-#chat_actions ul#message_preview li div.bubble div.message > p {
+ul#message_preview li div.bubble div.message > p {
white-space: initial;
display: -webkit-box;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
}
-#chat_actions ul#message_preview li.reactions {
+ul#message_preview li.reactions {
padding: 0 0.5rem;
padding-bottom: 0.75rem;
}
-#chat_actions ul#message_preview li.reactions > p {
+ul#message_preview li.reactions > p {
line-height: 4.75rem;
}
\ No newline at end of file
diff --git a/app/Widgets/ChatActions/chatactions.js b/app/Widgets/ChatActions/chatactions.js
index 41dbe1e88..b5e471415 100644
--- a/app/Widgets/ChatActions/chatactions.js
+++ b/app/Widgets/ChatActions/chatactions.js
@@ -3,5 +3,15 @@ var ChatActions = {
setMessage: function (message) {
ChatActions.message = message;
+ },
+ focusSearch: function () {
+ document.querySelector('form[name=search] input').focus();
}
}
+
+MovimEvents.registerWindow('keydown', 'search_message', (e) => {
+ if (e.key == 'f' && e.ctrlKey && MovimUtils.urlParts().page == 'chat' && Chat.getTextarea()) {
+ e.preventDefault();
+ ChatActions_ajaxShowSearchDialog(Chat.getTextarea().dataset.jid, Boolean(Chat.getTextarea().dataset.muc));
+ }
+});
diff --git a/app/Widgets/ChatActions/locales.ini b/app/Widgets/ChatActions/locales.ini
index c5fc2ca37..c1f71c0bd 100644
--- a/app/Widgets/ChatActions/locales.ini
+++ b/app/Widgets/ChatActions/locales.ini
@@ -1,3 +1,5 @@
[chatactions]
copy_text = Copy the text
copied_text = Text copied
+search_messages = Search messages by content
+search_messages_empty = No messages found
\ No newline at end of file
diff --git a/app/Widgets/Search/_search.tpl b/app/Widgets/Search/_search.tpl
index ed0b7bb34..41144e1ab 100644
--- a/app/Widgets/Search/_search.tpl
+++ b/app/Widgets/Search/_search.tpl
@@ -35,7 +35,7 @@
search
-
{$c->__('search.subtitle')}
+ {$c->__('input.open_me_using')} Ctrl + M
diff --git a/app/Widgets/Search/_search_results_empty.tpl b/app/Widgets/Search/_search_results_empty.tpl
index cafb7aa1a..ca8a1a7d8 100644
--- a/app/Widgets/Search/_search_results_empty.tpl
+++ b/app/Widgets/Search/_search_results_empty.tpl
@@ -5,6 +5,6 @@
{else}
search
-
{$c->__('search.subtitle')}
+ {$c->__('input.open_me_using')} Ctrl + M
{/if}
diff --git a/app/Widgets/Search/locales.ini b/app/Widgets/Search/locales.ini
index 4c7b684a5..a23212f48 100644
--- a/app/Widgets/Search/locales.ini
+++ b/app/Widgets/Search/locales.ini
@@ -1,6 +1,5 @@
[search]
keyword = What are you looking for?
-subtitle = Open me using Ctrl + M
placeholder = "#cats, username@server.com, John…"
no_contacts_title = No contacts yet?
no_contacts_text = Find one by searching for their name or address
diff --git a/database/migrations/20250806055432_add_body_ts_vector_to_messages_table.php b/database/migrations/20250806055432_add_body_ts_vector_to_messages_table.php
new file mode 100644
index 000000000..7699aa629
--- /dev/null
+++ b/database/migrations/20250806055432_add_body_ts_vector_to_messages_table.php
@@ -0,0 +1,26 @@
+schema->getConnection()->getDriverName() == 'pgsql') {
+ $this->schema->table('messages', function (Blueprint $table) {
+ DB::statement("create index messages_body_gin_index on messages using gin (to_tsvector('simple', body));");
+ });
+ }
+ }
+
+ public function down()
+ {
+ if ($this->schema->getConnection()->getDriverName() == 'pgsql') {
+ $this->schema->table('messages', function (Blueprint $table) {
+ $table->dropIndex('messages_body_gin_index');
+ });
+ }
+ }
+}
diff --git a/locales/locales.ini b/locales/locales.ini
index 23a2e3e2e..ad3541bcb 100644
--- a/locales/locales.ini
+++ b/locales/locales.ini
@@ -95,6 +95,7 @@ username = Username
password = Password
optional = Optional
muc_pubsub_node = Associated Community
+open_me_using = "Open me using"
[day]
title = Day
diff --git a/public/theme/css/chip.css b/public/theme/css/chip.css
index 43c937a35..a7a576c31 100644
--- a/public/theme/css/chip.css
+++ b/public/theme/css/chip.css
@@ -12,13 +12,14 @@
text-overflow: ellipsis;
white-space: nowrap;
max-width: 80%;
+ vertical-align: middle;
}
-*:not(p)>.chip:first-child {
+*:not(p):not(h4)>.chip:first-child {
margin-left: 0;
}
-*:not(p)>.chip:last-child {
+*:not(p):not(h4)>.chip:last-child {
margin-right: 0;
}
diff --git a/public/theme/css/color.css b/public/theme/css/color.css
index 4f1c7921a..f70a69312 100644
--- a/public/theme/css/color.css
+++ b/public/theme/css/color.css
@@ -187,6 +187,7 @@ header.big ul.list li>div>p>span.second {
input[type=button].color,
header.big,
+mark,
main > header {
background-color: var(--movim-accent);
border-color: var(--movim-accent);
diff --git a/src/Movim/Widget/Base.php b/src/Movim/Widget/Base.php
index 85758eda7..10f9aa263 100644
--- a/src/Movim/Widget/Base.php
+++ b/src/Movim/Widget/Base.php
@@ -13,6 +13,8 @@ use Moxl\Xec\Payload\Packet;
use WyriHaximus\React\Cron;
use WyriHaximus\React\Cron\Action;
+use Illuminate\Database\Capsule\Manager as DB;
+
class Base
{
protected array $js = []; // Contains javascripts
@@ -116,6 +118,11 @@ class Base
return \Movim\Route::urlize(...$args);
}
+ public function database(string $driver): bool
+ {
+ return DB::getDriverName() == $driver;
+ }
+
public function rpc(...$args)
{
\Movim\RPC::call(...$args);