show unread mail count in topbar
authorxangelo <git@xangelo.ca>
Tue, 7 Jun 2022 03:11:24 +0000 (23:11 -0400)
committerxangelo <git@xangelo.ca>
Tue, 7 Jun 2022 03:11:24 +0000 (23:11 -0400)
The system tracks your unread mail and displays it to you on the menu
bar. Reading your unread mail will decrease this count to 0. At 0, the
badge next to the mail link disappears.

public/game.html
public/scifi.css
src/api.ts
src/render/mail.ts
src/render/topbar.ts
src/repository/mail.ts

index 2ac499877f5ec10e449b96f134d4e6313a0d6e9a..1115b82cd95d5be9aeb22c1e6a55929e99c61a3f 100644 (file)
@@ -38,7 +38,7 @@
                                 <a href="#" hx-target="#main" hx-post="/poll/map" hx-trigger="click">Map</a>
                             </li>
                             <li>
-                                <a href="#" hx-target="#main" hx-get="/poll/mailroom" hx-trigger="click">Mail</a>
+                                <a href="#" hx-target="#main" hx-get="/poll/mailroom" hx-trigger="click" id="mail-link">Mail</a>
                             </li>
                         </ul>
                     </div>
index 98c99bd372922779a6c911ff779b07810f4f7b61..ce21ee15a8d92c707949e566b7c3ce84d76737c0 100644 (file)
@@ -191,6 +191,7 @@ footer {
 }
 #nav li a {
     padding: 10px 25px;
+    position: relative;
 }
 #nav li a:hover {
     background-color: var(--border);
@@ -416,6 +417,18 @@ form > div {
   margin-bottom: 5px;
 }
 
+.badge {
+  border-radius: 5px;
+  padding: 0 6px;
+  font-size: 0.7rem;
+  position: absolute;
+  top: 0;
+  right: 0;
+}
+.badge.danger {
+  background-color: var(--red-border);
+}
+
 #stats {
   margin-top: 1rem;
 }
index 66381943a40e55598146862be56975ebd26dbbad..5b1edc657a5969056141dc3190231d429106e519 100644 (file)
@@ -100,6 +100,8 @@ server.get<{params: { cityId: string }}, string>('/city/:cityId', async req => {
 server.get<{}, string>('/poll/overview', async req => {
        const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
        const city = await cityRepo.getUsersCity(account.id);
+  const unreadMail = await mailRepo.countUnread(account.id);
+
 
   const usage = {
     foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
@@ -111,13 +113,14 @@ server.get<{}, string>('/poll/overview', async req => {
        return renderKingomOverview({
     ...city,
     ...usage
-  }, account) + topbar({...city, ...usage});
+  }, account) + topbar({...city, ...usage}, unreadMail);
 });
 
 server.get<{}, string>('/poll/construction', async req => {
        const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
        const city = await cityRepo.getUsersCity(account.id);
        const buildings = await cityRepo.buildingRepository.list();
+  const unreadMail = await mailRepo.countUnread(account.id);
 
        const buildQueues = await cityRepo.getBuildQueues(account.id);
   const usage = {
@@ -126,12 +129,13 @@ server.get<{}, string>('/poll/construction', async req => {
     energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
     energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
   }
-       return renderLandDevelopment(city, buildings, buildQueues) + topbar({...city, ...usage});
+       return renderLandDevelopment(city, buildings, buildQueues) + topbar({...city, ...usage}, unreadMail);
 });
 
 server.get<{}, string>('/poll/unit-training', async req => {
        const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
        const city = await cityRepo.getUsersCity(account.id);
+  const unreadMail = await mailRepo.countUnread(account.id);
 
        const unitTrainingQueues = await cityRepo.getUnitTrainingQueues(account.id);
        const units = await cityRepo.unitRepository.list();
@@ -145,12 +149,13 @@ server.get<{}, string>('/poll/unit-training', async req => {
        return renderUnitTraining(city, units, unitTrainingQueues) + topbar({
     ...city,
     ...usage
-  });
+  }, unreadMail);
 });
 
 server.post<{body: {sector: string}}, string>('/poll/map', async req => {
        const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
        const city = await cityRepo.getUsersCity(account.id);
+  const unreadMail = await mailRepo.countUnread(account.id);
 
   let sector = city.sector_id;
   if(req.body.sector) {
@@ -172,12 +177,13 @@ server.post<{body: {sector: string}}, string>('/poll/map', async req => {
        return renderOverworldMap(await cityRepo.findAllInSector(sector), city, sector) + topbar({
     ...city,
     ...usage
-  });
+  }, unreadMail);
 });
 
 server.get<{}, string>('/poll/mailroom', async req => {
        const account = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
        const city = await cityRepo.getUsersCity(account.id);
+  const unreadMail = await mailRepo.countUnread(account.id);
 
   const usage = {
     foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
@@ -189,7 +195,7 @@ server.get<{}, string>('/poll/mailroom', async req => {
        return renderMailroom(await mailRepo.listReceivedMessages(account.id)) + topbar({
     ...city,
     ...usage
-  });
+  }, unreadMail);
 });
 
 
@@ -336,24 +342,29 @@ server.post<{
                });
        }, 'reload-outgoing-attacks');
 
-server.get<void, string>('/messages', async req => {
-       const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
-       const msgs = await mailRepo.listReceivedMessages(acct.id);
-
-       return JSON.stringify(msgs);
-});
-
 server.get<{params: {id: string}}, string>('/messages/:id', async req => {
        const acct = await accountRepo.validate(req.authInfo.accountId, req.authInfo.token);
+  const city = await cityRepo.getUsersCity(acct.id);
        const msg = await mailRepo.getMessage(req.params.id, acct.id);
 
        if(!msg) {
                throw new NotFoundError('No such message', ERROR_CODE.DUPLICATE_CACHE_KEY);
        }
 
+  const usage = {
+    foodUsagePerTick: await cityRepo.foodUsagePerTick(city),
+    foodProductionPerTick: await cityRepo.foodProductionPerTick(city),
+    energyUsagePerTick: await cityRepo.energyUsagePerTick(city),
+    energyProductionPerTick: await cityRepo.energyProductionPerTick(city)
+  }
+
        await mailRepo.markAsRead(msg.id, msg.to_account);
+  const unreadMail = await mailRepo.countUnread(acct.id);
 
-       return renderMessage(msg);
+       return renderMailroom(await mailRepo.listReceivedMessages(acct.id), msg) + topbar({
+    ...city,
+    ...usage
+  }, unreadMail);
 });
 
 server.get<void, string>('/attacks/outgoing', async req => {
index a0b3b753c4e05b3bfa497660ff3a570e37134207..0048b59db83fcdda28cd54f90947dd2dc2451984 100644 (file)
@@ -1,15 +1,16 @@
 import { DateTime } from "luxon";
 import { MessageWithNames } from "../repository/mail";
 
-export function renderMailroom(mail: MessageWithNames[]): string {
+export function renderMailroom(mail: MessageWithNames[], msg?: MessageWithNames): string {
     return `
-    <div hx-trigger="every 600s" hx-get="/poll/mailroom">
+    <div hx-trigger="every 600s" hx-get="/poll/mailroom" hx-swap-oob="true" id="main">
     <h2 data-augmented-ui="tl-clip bl-clip none">Mail</h2>
     <table>
     <tr>
         <th>From</th>
         <th>Subject</th>
         <th>Sent At</th>
+        <th>Read At</th>
     </tr>
     ${mail.map(msg => {
         return `
@@ -21,11 +22,12 @@ export function renderMailroom(mail: MessageWithNames[]): string {
             </a>
             </td>
             <td>${DateTime.fromMillis(msg.sent_at)}</td>
+            <td>${msg.read_at === 0 ? 'Unread' : DateTime.fromMillis(msg.read_at)}</td>
         </tr>
         `;
     }).join("\n")}
     </table>
-    <div id="individual-message"></div>
+    <div id="individual-message">${msg ? renderMessage(msg) : ''}</div>
     </div>
     `;
 }
index 94313c55f411b90cd4f5936de818491dc7bde61a..a6dc789d273f3d090e07948ccd764d9956c38365 100644 (file)
@@ -7,11 +7,22 @@ type Usage = {
   energyProductionPerTick: number;
 }
 
+function renderUnreadBadge(count: number): string {
+  return `<span class="badge danger">${count}</span>`;
 
-export function topbar(city: City & Usage): string {
+}
+
+function mailLink(unreadCount: number): string {
+  return `
+  <a href="#" hx-target="#main" hx-get="/poll/mailroom" hx-trigger="click" id="mail-link" hx-swap-oob="true">Mail ${unreadCount > 0 ? renderUnreadBadge(unreadCount) : ''}</a>
+  `;
+}
+
+export function topbar(city: City & Usage, unreadMail: number): string {
   const foodRateOfChange = city.foodProductionPerTick - city.foodUsagePerTick;
   const energyRateOfChange = city.energyProductionPerTick - city.energyUsagePerTick;
     const oob = `
+    ${mailLink(unreadMail)}
     <div class="col" id="info-bar" hx-swap-oob="true">
     <span>
       <b data-augmented-ui="all-hex border" title="Credits">&#8353;</b>
index bf480c03fdf15b5ec332090f9f130db2f5949836..0fe530bb0994143094788447b223508d903c03bb 100644 (file)
@@ -56,6 +56,19 @@ export class MailRepository extends Repository<Message> {
         return res.pop();
     }
 
+    async countUnread(to: string): Promise<number> {
+      const res = await this.db.raw<{unread: number}>(`select count(id) as 
+        unread from mail 
+        where to_account = ? and read_at = 0`, to);
+
+      try {
+        return parseInt(res[0].unread.toString()) || 0;
+      }
+      catch(e) {
+        return 0;
+      }
+    }
+
     async listReceivedMessages(to: string): Promise<MessageWithNames[]> {
         return this.db.raw<MessageWithNames[]>(`select m.*, a.username 
         from mail m 
@@ -63,4 +76,4 @@ export class MailRepository extends Repository<Message> {
         where m.to_account = ? 
         order by sent_at desc`, to);
     }
-}
\ No newline at end of file
+}