見出し画像

inertia.js+react通知(3) - UI

未読の通知数を出し、それを一覧するようなもの。その先のことは後々決めていく。

未読の数を収集

ここではunreadNotifications を利用する。まずtinkerで試してみてもいいかも

> $u = User::find(1)
[!] Aliasing 'User' to 'App\Models\User' for this Tinker session.
= App\Models\User {#6406
    id: 1,
    name: "Admin User",
    email: "admin@example.com",
    email_verified_at: "2024-05-08 13:55:43",
    last_login_at: "2024-05-08 13:59:34",
    #password: "$2y$12$WBm8qcgFnFwfYQ9G4AwZyOAMJpXAqJJGZRtql949kQZj58cWESNYO",
    #remember_token: "fx3j0gPqdX",
    created_at: "2024-05-08 13:55:43",
    updated_at: "2024-05-08 13:59:34",
  }

> $u->unreadNotifications->count()
= 3

こんな感じで使ってあげる。これを app/Http/Middleware/HandleInertiaRequests.php に書く

    public function share(Request $request): array
    {
        return [
            ...parent::share($request),
            'auth' => [
                'user'    => $request->user(),
                'isAdmin' => $request->user()?->hasRole('admin'),
            ],
            'locale'  => app()->getLocale(),
            'notificationsCount' => $request->user() ? $request->user()->unreadNotifications->count() : 0,
        ];
    }


未読数が出た

処理したqueueの数だけ出ているはずだ。まあ実際には数は未読要素をcountすればいいのでこのようにcountだけpropで渡す必要はないのだろうけど、今のところ一応。

で、これをdropdownにしていく

ドロップダウンを作る

でまあ、通知を放置して100件とかなるとこのdropdownがしんどい事になるので、本来は無限スクロールみたいな(noteみたいな)のを取り入れたいところだけどこいつの実装は面倒なので、とりあえずここではそれは無しにしよう。

app/Http/Middleware/HandleInertiaRequests.php 

    public function share(Request $request): array
    {
        $notifications = function () use ($request) {
            return $request->user()
                ? $request->user()->notifications->map(function ($notification) {
                    return [
                        'id' => $notification->id,
                        'type' => $notification->type,
                        'data' => $notification->data,
                        'read_at' => $notification->read_at,
                        'created_at' => $notification->created_at->toDateTimeString(),
                    ];
                })
                : [];
        };

        dd($notifications());

これはこんな感じの構造体となっている

Illuminate\Support\Collection {#1509 ▼ // app/Http/Middleware/HandleInertiaRequests.php:46
  #items: array:3 [▼
    0 => array:5 [▼
      "id" => "18877b90-3f3f-4004-b81d-8345886b60d9"
      "type" => "App\Notifications\FileAnalyzedNotification"
      "data" => array:3 [▶]
      "read_at" => null
      "created_at" => "2024-05-08 15:49:36"
    ]
    1 => array:5 [▼
      "id" => "79493bc4-cb11-4571-91c5-5a6818d42a2e"
      "type" => "App\Notifications\FileAnalyzedNotification"
      "data" => array:3 [▶]
      "read_at" => null
      "created_at" => "2024-05-08 15:49:26"
    ]
    2 => array:5 [▼
      "id" => "22639400-ff19-4f63-b541-46d348797aac"
      "type" => "App\Notifications\FileAnalyzedNotification"
      "data" => array:3 [▶]
      "read_at" => null
      "created_at" => "2024-05-08 15:49:07"
    ]
  ]
  #escapeWhenCastingToString: false
}

最終的に

    public function share(Request $request): array
    {
        $notifications = $request->user()
            ? $request->user()->notifications->map(function ($notification) {
                return [
                    'id' => $notification->id,
                    'type' => $notification->type,
                    'data' => $notification->data,
                    'read_at' => $notification->read_at,
                    'created_at' => $notification->created_at->toDateTimeString(),
                ];
            })
        : [];

        return [
            ...parent::share($request),
            'auth' => [
                'user'    => $request->user(),
                'isAdmin' => $request->user()?->hasRole('admin'),
            ],
            'locale'  => app()->getLocale(),
            'notifications' => $notifications,
        ];
    }

ここでcountを捨てている

frontendの加工

resources/js/Layouts/AuthenticatedLayout.jsx 

  const { props: { auth, notifications } } = usePage();
  const { t, currentLocale } = useLaravelReactI18n();
  const [showingNavigationDropdown, setShowingNavigationDropdown] = useState(false);
  const [isNotificationsOpen, setIsNotificationsOpen] = useState(false);
  const [notificationList, setNotificationList] = useState(notifications);
  const notificationsCount = notifications.length;

で取ってくる。ポイントは特に

 const [notificationList, setNotificationList] = useState(notifications);

ここでstateにnotificationをつめこんでlist化していることだ。そして

 const notificationsCount = notifications.length;

countを動的に計算している。

最終的なUI

逐一解説しながら作ってくのが面倒になったので、ここではまず完成したものを出してみよう

resources/js/Layouts/AuthenticatedLayout.jsx (抜粋)


<div className="hidden sm:flex sm:items-center sm:ms-6">
  <div className="ms-3 relative">
    <span className="inline-flex rounded-md">
      <button
        type="button"
        className="inline-flex items-center p-2 ml-3 border border-transparent rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150"
        onClick={() => setIsNotificationsOpen(!isNotificationsOpen)}
      >
        <VscBell className="h-6 w-6" />
        {notificationsCount > 0 && (
        <span className="absolute top-0 right-0 block h-5 w-5 text-xs leading-5 rounded-full text-center text-white bg-red-600">
          {notificationList.length > 9 ? '10+' : notificationList.length}
        </span>
        )}
      </button>
    </span>
    {isNotificationsOpen && (
    <div className="absolute right-0 mt-2 py-2 w-64 bg-white rounded-md shadow-xl">
      <ul>
        <li key="clear-all" className="px-4 py-2 border-b border-gray-100 hover:bg-gray-50" onClick={clearNotifications}>
          {t("Clear Notifications")}
        </li>
        {notificationList.map((notification) => (
          <li key={notification.id} className="px-4 py-2 border-b border-gray-100 hover:bg-gray-50"
          onClick={() => handleNotificationClick(notification.id)}>
          {notification.data.message}
        </li>
        ))}
      </ul>
    </div>
    )}
  </div>

削除の設置

<li key="clear-all" className="px-4 py-2 border-b border-gray-100 hover:bg-gray-50" onClick={clearNotifications}>
  {t("Clear Notifications")}
</li>

これ

Clear Notificationsが表示された

要するにliの要素を一番上にはめこんでいるだけなんだけど、ここで 

onClick={clearNotifications}

としているのでclearNotifications関数が必要だ。

  const clearNotifications = () => {
    router.get(route('notifications.clear'));
  };

ここは単純にrouter.getしている。ではこのnotification.clearというrouteは何なのかというと

routes/web.php 

Route::get('/notifications/clear', function () {
    auth()->user()->unreadNotifications->markAsRead();
    return redirect()->back();
})->middleware(['auth', 'verified'])->name('notifications.clear');

このように auth()->user()->unreadNotifications->markAsRead(); という魔法をかけると当該ユーザーの通知が全て既読になるので、それを行ってredirectで戻している。それだけ。

各要素を既読にしてredirectする処置

では各要素を見てみると

{notificationList.map((notification) => (
  <li key={notification.id} className="px-4 py-2 border-b border-gray-100 hover:bg-gray-50"
    onClick={() => handleNotificationClick(notification.id)}>
    {notification.data.message}
  </li>
))}

ここでloopしているのがわかるとおもうが、onClick={() => handleNotificationClick(notification.id)}> こんなのが付いた、ってことはhandleNotificationClick関数を見よう

  const handleNotificationClick = (notificationId) => {

    // 通知を削除する処理
    const newNotifications = notificationList.filter(notification => notification.id !== notificationId);
    setNotificationList(newNotifications);

    router.get(route('notification.redirect', {
      notification: notificationId,
    }));
  };

ここではnotification.redirect関数に遅りこんでいるのだが、その前に通知のリストを削除し動的に更新できるようにしている。それは

    const newNotifications = notificationList.filter(notification => notification.id !== notificationId);
    setNotificationList(newNotifications);

この2行でstateにつめこんでおいた配列リストから当該を削除しているということであり、またcountに関してはこのstateから計算されているから、自動的に数は変動するだろう。最終的にrouter.getでbackendに送っている。これがどうなっているかというと

routes/web.php 

Route::get('/notification/rd/{notification}', function ($id) {
    $notification = auth()->user()->unreadNotifications->find($id);
    if ($notification->notifiable_id !== auth()->id()) {
        abort(403, 'Unauthorized action.');
    }

    $notification->markAsRead();
    $url = $notification->data['url'] ?? '/';

    return redirect($url);
})->middleware(['auth', 'verified'])->name('notification.redirect');

こんな感じになっている。このurlパラメーターはlaravelの各通知class郡でセットしたカスタムの配列キーである。それを取り出してredirectしているという、それだけの処理

見た目をもうちょい何とかしたいよねって話

まあ、このままだと頑張ってこんなくらいですかねえ…

<ul>
  <li key="clear-all"
      className="px-4 py-2 border-b border-gray-200 bg-red-100 text-red-600 flex items-center cursor-pointer hover:bg-red-200"
      onClick={clearNotifications}>
    <span className="mr-2"><VscChromeClose /></span>
    {t("Clear Notifications")}
  </li>
  {notificationList.map((notification) => (
    <li key={notification.id}
        className="px-4 py-2 border-b-2 border-gray-200 hover:bg-gray-100 flex items-center cursor-pointer"
        onClick={() => handleNotificationClick(notification.id)}>
      <span className="text-gray-600 mr-2"><VscChevronRight /></span>
      {notification.data.message}
    </li>
  ))}
</ul>




この記事が気に入ったらサポートをしてみませんか?