diff --git a/lib/Controller/APIv2Controller.php b/lib/Controller/APIv2Controller.php old mode 100644 new mode 100755 index 8ac519c10..7796308c3 --- a/lib/Controller/APIv2Controller.php +++ b/lib/Controller/APIv2Controller.php @@ -51,6 +51,9 @@ class APIv2Controller extends OCSController { /** @var bool */ protected $loadPreviews; + /** @var string */ + protected $search = ''; + public function __construct( $appName, IRequest $request, @@ -79,7 +82,7 @@ public function __construct( * @throws InvalidFilterException when the filter is invalid * @throws \OutOfBoundsException when no user is given */ - protected function validateParameters($filter, $since, $limit, $previews, $objectType, $objectId, $sort) { + protected function validateParameters($filter, $since, $limit, $previews, $objectType, $objectId, $sort, $search = '') { $this->filter = \is_string($filter) ? $filter : 'all'; if ($this->filter !== $this->data->validateFilter($this->filter)) { throw new InvalidFilterException('Invalid filter'); @@ -90,6 +93,7 @@ protected function validateParameters($filter, $since, $limit, $previews, $objec $this->objectType = (string)$objectType; $this->objectId = (int)$objectId; $this->sort = \in_array($sort, ['asc', 'desc'], true) ? $sort : 'desc'; + $this->search = \is_string($search) ? trim($search) : ''; if (($this->objectType !== '' && $this->objectId === 0) || ($this->objectType === '' && $this->objectId !== 0)) { // Only allowed together @@ -117,8 +121,8 @@ protected function validateParameters($filter, $since, $limit, $previews, $objec * @param string $sort * @return DataResponse */ - public function getDefault($since = 0, $limit = 50, $previews = false, $object_type = '', $object_id = 0, $sort = 'desc'): DataResponse { - return $this->get('all', $since, $limit, $previews, $object_type, $object_id, $sort); + public function getDefault($since = 0, $limit = 50, $previews = false, $object_type = '', $object_id = 0, $sort = 'desc', $q = ''): DataResponse { + return $this->get('all', $since, $limit, $previews, $object_type, $object_id, $sort, $q); } /** @@ -131,10 +135,11 @@ public function getDefault($since = 0, $limit = 50, $previews = false, $object_t * @param string $object_type * @param int $object_id * @param string $sort + * @param string $q Free-text query — filters subject + message server-side * @return DataResponse */ - public function getFilter($filter, $since = 0, $limit = 50, $previews = false, $object_type = '', $object_id = 0, $sort = 'desc'): DataResponse { - return $this->get($filter, $since, $limit, $previews, $object_type, $object_id, $sort); + public function getFilter($filter, $since = 0, $limit = 50, $previews = false, $object_type = '', $object_id = 0, $sort = 'desc', $q = ''): DataResponse { + return $this->get($filter, $since, $limit, $previews, $object_type, $object_id, $sort, $q); } /** @@ -199,9 +204,9 @@ public function listFilters(): DataResponse { * @param string $sort * @return DataResponse */ - protected function get($filter, $since, $limit, $previews, $filterObjectType, $filterObjectId, $sort): DataResponse { + protected function get($filter, $since, $limit, $previews, $filterObjectType, $filterObjectId, $sort, $search = ''): DataResponse { try { - $this->validateParameters($filter, $since, $limit, $previews, $filterObjectType, $filterObjectId, $sort); + $this->validateParameters($filter, $since, $limit, $previews, $filterObjectType, $filterObjectId, $sort, $search); } catch (InvalidFilterException $e) { return new DataResponse([], Http::STATUS_NOT_FOUND); } catch (\OutOfBoundsException $e) { @@ -221,7 +226,9 @@ protected function get($filter, $since, $limit, $previews, $filterObjectType, $f $this->filter, $this->objectType, - $this->objectId + $this->objectId, + false, + $this->search, ); } catch (\OutOfBoundsException $e) { // Invalid since argument @@ -279,6 +286,9 @@ protected function generateHeaders(array $headers, bool $hasMoreActivities, arra $nextPageParameters['object_type'] = $this->objectType; $nextPageParameters['object_id'] = $this->objectId; } + if ($this->search !== '') { + $nextPageParameters['q'] = $this->search; + } if ($this->request->getParam('format') !== null) { $nextPageParameters['format'] = $this->request->getParam('format'); } diff --git a/lib/Data.php b/lib/Data.php old mode 100644 new mode 100755 index b4d444bdf..d6749493e --- a/lib/Data.php +++ b/lib/Data.php @@ -223,10 +223,13 @@ public function storeMail(IEvent $event, int $latestSendTime): bool { * @param int $objectId Allows to filter the activities to a given object. May only appear together with $objectType * * @param bool $returnEvents return only the events + * @param string $search Free-text query — when non-empty, only activities whose subject or message contains + * this substring (case-insensitive) are returned. Used for the server-side search box + * in the activity stream UI. * @return array * */ - public function get(GroupHelper $groupHelper, UserSettings $userSettings, $user, $since, $limit, $sort, $filter, $objectType = '', $objectId = 0, bool $returnEvents = false) { + public function get(GroupHelper $groupHelper, UserSettings $userSettings, $user, $since, $limit, $sort, $filter, $objectType = '', $objectId = 0, bool $returnEvents = false, string $search = '') { // get current user if ($user === '') { throw new \OutOfBoundsException('Invalid user', 1); @@ -283,6 +286,21 @@ public function get(GroupHelper $groupHelper, UserSettings $userSettings, $user, } } + // Free-text search: match against subject and message columns. + // Trim + length-cap defensively so a pathological query can't blow up + // the SQL or pull every row through a LIKE on a giant string. + $search = trim($search); + if ($search !== '') { + $needle = '%' . $this->connection->escapeLikeParameter(mb_substr($search, 0, 200)) . '%'; + $param = $query->createNamedParameter($needle); + $query->andWhere( + $query->expr()->orX( + $query->expr()->iLike('subject', $param), + $query->expr()->iLike('message', $param), + ), + ); + } + /** * Order and specify the offset */ diff --git a/src/components/ActivityBurst.vue b/src/components/ActivityBurst.vue new file mode 100644 index 000000000..52b0acf17 --- /dev/null +++ b/src/components/ActivityBurst.vue @@ -0,0 +1,206 @@ + + + + + + + diff --git a/src/components/ActivityComponent.vue b/src/components/ActivityComponent.vue index 49de1b5ba..5beb4ce92 100644 --- a/src/components/ActivityComponent.vue +++ b/src/components/ActivityComponent.vue @@ -26,6 +26,12 @@ const props = defineProps<{ * Whether to show previews */ showPreviews: boolean + + /** + * Whether this activity arrived via polling (vs. initial load) and + * should briefly pulse to draw attention. + */ + fresh?: boolean }>() defineEmits(['reload']) diff --git a/src/components/ActivityGrid.vue b/src/components/ActivityGrid.vue old mode 100644 new mode 100755 index 64ead6640..8c210aaa7 --- a/src/components/ActivityGrid.vue +++ b/src/components/ActivityGrid.vue @@ -4,53 +4,86 @@ --> diff --git a/src/components/ActivityGroup.vue b/src/components/ActivityGroup.vue old mode 100644 new mode 100755 index b04deed8e..dd33d0f20 --- a/src/components/ActivityGroup.vue +++ b/src/components/ActivityGroup.vue @@ -4,16 +4,26 @@ --> + + diff --git a/src/components/activities/GenericActivity.vue b/src/components/activities/GenericActivity.vue old mode 100644 new mode 100755 index 3e0b1e04b..3b5bcb11c --- a/src/components/activities/GenericActivity.vue +++ b/src/components/activities/GenericActivity.vue @@ -4,14 +4,47 @@ -->