Skip to content

Commit

Permalink
implement notifications and custom handling in the web UI (open resul…
Browse files Browse the repository at this point in the history
…ts in a modal)

Signed-off-by: Julien Veyssier <[email protected]>
  • Loading branch information
julien-nc committed Aug 3, 2023
1 parent 7d1fd96 commit ac1161a
Show file tree
Hide file tree
Showing 9 changed files with 237 additions and 24 deletions.
2 changes: 1 addition & 1 deletion appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
['name' => 'config#setAdminConfig', 'url' => '/admin-config', 'verb' => 'PUT'],

['name' => 'assistant#getplop', 'url' => '/plop', 'verb' => 'GET'],
['name' => 'assistant#getTaskResultPage', 'url' => '/t/{id}', 'verb' => 'GET'],
['name' => 'assistant#getTaskResultPage', 'url' => '/t/{taskId}', 'verb' => 'GET'],
],
];
12 changes: 6 additions & 6 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

namespace OCA\TPAssistant\AppInfo;

use OCA\TPAssistant\Listener\BeforeTemplateRenderedListener;
use OCA\TPAssistant\Listener\TaskFailedListener;
use OCA\TPAssistant\Listener\TaskSuccessfulListener;
use OCA\TPAssistant\Notification\Notifier;
use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent;
use OCP\IConfig;

use OCP\AppFramework\App;
Expand All @@ -12,7 +15,6 @@
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\TextProcessing\Events\TaskFailedEvent;
use OCP\TextProcessing\Events\TaskSuccessfulEvent;
use OCP\Util;

class Application extends App implements IBootstrap {

Expand All @@ -28,16 +30,14 @@ public function __construct(array $urlParams = []) {
}

public function register(IRegistrationContext $context): void {
$context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class);

$context->registerEventListener(TaskSuccessfulEvent::class, TaskSuccessfulListener::class);
$context->registerEventListener(TaskFailedEvent::class, TaskFailedListener::class);
$context->registerNotifierService(Notifier::class);
}

public function boot(IBootContext $context): void {
$context->injectFn(function (
$userId
) {
Util::addScript(self::APP_ID, self::APP_ID . '-assistant');
});
}
}

5 changes: 2 additions & 3 deletions lib/Controller/AssistantController.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,8 @@ public function __construct(
*/
#[NoAdminRequired]
#[NoCSRFRequired]
public function getImageGenerationPage(string $hash): TemplateResponse {
$generationData = $this->assistantService->getGenerationInfo($hash);
$this->initialStateService->provideInitialState('generation', $generationData);
public function getTaskResultPage(int $taskId): TemplateResponse {
$this->initialStateService->provideInitialState('taskId', $taskId);
return new TemplateResponse(Application::APP_ID, 'taskResultPage');
}
}
39 changes: 39 additions & 0 deletions lib/Listener/BeforeTemplateRenderedListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace OCA\TPAssistant\Listener;

use OCA\TPAssistant\AppInfo\Application;
use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\IUser;
use OCP\IUserSession;
use OCP\Util;

class BeforeTemplateRenderedListener implements IEventListener {

public function __construct(
private IUserSession $userSession,
) {
}

public function handle(Event $event): void {
if (!($event instanceof BeforeTemplateRenderedEvent)) {
// Unrelated
return;
}

if ($event->getResponse()->getRenderAs() !== TemplateResponse::RENDER_AS_USER) {
return;
}

if (!$this->userSession->getUser() instanceof IUser) {
return;
}

Util::addScript(Application::APP_ID, Application::APP_ID . '-assistant');
}
}
4 changes: 4 additions & 0 deletions lib/Listener/TaskFailedListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

namespace OCA\TPAssistant\Listener;

use OCA\TPAssistant\Service\AssistantService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\TextProcessing\Events\TaskFailedEvent;

class TaskFailedListener implements IEventListener {

public function __construct(
private AssistantService $assistantService,
) {
}

Expand All @@ -17,6 +19,8 @@ public function handle(Event $event): void {
return;
}

$task = $event->getTask();
$this->assistantService->sendNotification($task);
error_log('Task failed');
}
}
4 changes: 4 additions & 0 deletions lib/Listener/TaskSuccessfulListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

namespace OCA\TPAssistant\Listener;

use OCA\TPAssistant\Service\AssistantService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\TextProcessing\Events\TaskSuccessfulEvent;

class TaskSuccessfulListener implements IEventListener {

public function __construct(
private AssistantService $assistantService,
) {
}

Expand All @@ -17,6 +19,8 @@ public function handle(Event $event): void {
return;
}

$task = $event->getTask();
$this->assistantService->sendNotification($task);
error_log('Task successful');
}
}
95 changes: 95 additions & 0 deletions lib/Notification/Notifier.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

namespace OCA\TPAssistant\Notification;

use InvalidArgumentException;
use OCA\TPAssistant\AppInfo\Application;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\L10N\IFactory;
use OCP\Notification\IAction;
use OCP\Notification\INotification;

use OCP\Notification\INotifier;

class Notifier implements INotifier {

public function __construct(
private IFactory $factory,
private IUserManager $userManager,
private IURLGenerator $url,
private ?string $userId,
) {
}

/**
* Identifier of the notifier, only use [a-z0-9_]
*
* @return string
* @since 17.0.0
*/
public function getID(): string {
return Application::APP_ID;
}
/**
* Human readable name describing the notifier
*
* @return string
* @since 17.0.0
*/
public function getName(): string {
return $this->factory->get(Application::APP_ID)->t('Nextcloud assistant');
}

/**
* @param INotification $notification
* @param string $languageCode The code of the language that should be used to prepare the notification
* @return INotification
* @throws InvalidArgumentException When the notification was not prepared by a notifier
* @since 9.0.0
*/
public function prepare(INotification $notification, string $languageCode): INotification {
if ($notification->getApp() !== Application::APP_ID) {
// Not my app => throw
throw new InvalidArgumentException();
}

$l = $this->factory->get(Application::APP_ID, $languageCode);

$params = $notification->getSubjectParameters();
$iconUrl = $this->url->getAbsoluteURL($this->url->imagePath(Application::APP_ID, 'app-dark.svg'));

switch ($notification->getSubject()) {
case 'success':

$subject = $l->t('Assistant Task for app %1$s has finished', [$params['appId']]);
$content = $l->t('The input was: %1$s', [$params['input']]);
$link = $this->url->linkToRouteAbsolute(Application::APP_ID . '.assistant.getTaskResultPage', ['taskId' => $params['id']]);

$notification
->setParsedSubject($subject)
->setParsedMessage($content)
->setLink($link)
->setIcon($iconUrl);

$actionLabel = $l->t('View results');
$action = $notification->createAction();
$action->setLabel($actionLabel)
->setParsedLabel($actionLabel)
->setLink($notification->getLink(), IAction::TYPE_WEB)
->setPrimary(true);

$notification->addParsedAction($action);

return $notification;

case 'failure':
return $notification;

default:
// Unknown subject => Unknown notification => throw
throw new InvalidArgumentException();
}
}
}
33 changes: 27 additions & 6 deletions lib/Service/AssistantService.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,39 @@

namespace OCA\TPAssistant\Service;

use OCP\IConfig;
use OCP\IL10N;
use Psr\Log\LoggerInterface;
use DateTime;
use OCA\TPAssistant\AppInfo\Application;
use OCP\TextProcessing\Task;
use OCP\Notification\IManager as INotificationManager;

class AssistantService {

public function __construct(
string $appName,
private LoggerInterface $logger,
private IL10N $l10n,
private IConfig $config,
private INotificationManager $notificationManager,
) {
}

public function sendNotification(Task $task): void {
$manager = $this->notificationManager;
$notification = $manager->createNotification();

$params = [
'appId' => $task->getAppId(),
'id' => $task->getId(),
'input' => $task->getInput(),
];
$status = $task->getStatus();
$subject = $status === Task::STATUS_SUCCESSFUL
? 'success'
: 'failure';
$notification->setApp(Application::APP_ID)
->setUser($task->getUserId())
->setDateTime(new DateTime())
->setObject('task', $task->getId())
->setSubject($subject, $params);

$manager->notify($notification);
}

}
67 changes: 59 additions & 8 deletions src/assistant.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@ import { getRequestToken } from '@nextcloud/auth'
__webpack_nonce__ = btoa(getRequestToken()) // eslint-disable-line
__webpack_public_path__ = linkTo('textprocessing_assistant', 'js/') // eslint-disable-line

/**
* Creates an assistant modal and return a promise which provides the result
*
* @param {boolean} isInsideViewer Should be true if this function is called while the Viewer is displayed
* @return {Promise<unknown>}
*/
export async function openAssistant(appId, identifier = '', taskType = null, inputText = '', isInsideViewer = undefined) {
// Creates an assistant modal and return a promise which provides the result
// TODO jsdoc
// OCA.TPAssistant.openTextProcessingModal('app1', 'IDID', 'OCP\\TextProcessing\\SummaryTaskType', 'megainput').then(r => {console.debug('yeyeyeyeyeye', r)})
export async function openTextProcessingModal(appId, identifier = '', taskType = null, inputText = '', isInsideViewer = undefined) {
const { default: Vue } = await import(/* webpackChunkName: "vue-lazy" */'vue')
const { default: AssistantModal } = await import(/* webpackChunkName: "assistant-modal-lazy" */'./components/AssistantModal.vue')
Vue.mixin({ methods: { t, n } })
Expand Down Expand Up @@ -61,15 +58,69 @@ async function scheduleTask({ appId, identifier, taskType, input }) {
return axios.post(url, params)
}

function handleNotification(event) {
console.debug('aaaaaa -------- handle notification', event)
if (event.notification.app !== 'textprocessing_assistant' || event.action.type !== 'WEB') {
return
}
event.cancelAction = true
showResults(event.notification.objectId)
}

async function subscribeToNotifications() {
const { subscribe } = await import(/* webpackChunkName: "router-lazy" */'@nextcloud/event-bus')
subscribe('notifications:action:execute', handleNotification)
console.debug('aaaaa i subscribed', subscribe)
}

async function showResults(taskId) {
const { default: axios } = await import(/* webpackChunkName: "axios-lazy" */'@nextcloud/axios')
const { generateOcsUrl } = await import(/* webpackChunkName: "router-lazy" */'@nextcloud/router')
const url = generateOcsUrl('textprocessing/task/{taskId}', { taskId })
axios.get(url).then(response => {
console.debug('aaaaaa result of task', response.data)
openResultModal(response.data.ocs.data.task)
}).catch(error => {
console.error(error)
})
}

async function openResultModal(task) {
const { default: Vue } = await import(/* webpackChunkName: "vue-lazy" */'vue')
const { default: AssistantModal } = await import(/* webpackChunkName: "assistant-modal-lazy" */'./components/AssistantModal.vue')
Vue.mixin({ methods: { t, n } })

const modalId = 'assistantModal'
const modalElement = document.createElement('div')
modalElement.id = modalId
document.body.append(modalElement)

const View = Vue.extend(AssistantModal)
const view = new View({
propsData: {
// isInsideViewer,
input: task.input,
output: task.output,
selectedTaskTypeId: task.type,
readonly: true,
},
}).$mount(modalElement)

view.$on('cancel', () => {
view.$destroy()
})
}

function init() {
if (!OCA.TPAssistant) {
/**
* @namespace
*/
OCA.TPAssistant = {
openAssistant,
openTextProcessingModal,
}
}
}

init()
subscribeToNotifications()

0 comments on commit ac1161a

Please sign in to comment.