Skip to content

Commit

Permalink
Merge pull request #52 from tloncorp/ja/notif-reno
Browse files Browse the repository at this point in the history
garden: new notifications
  • Loading branch information
jamesacklin authored Jan 21, 2023
2 parents 6def14b + 38bd8af commit 2678ec6
Show file tree
Hide file tree
Showing 17 changed files with 501 additions and 75 deletions.
3 changes: 2 additions & 1 deletion ui/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ dist-ssr
*.local
stats.html
.eslintcache
.vercel
.vercel
storybook-static
17 changes: 17 additions & 0 deletions ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
},
"devDependencies": {
"@tailwindcss/aspect-ratio": "^0.2.1",
"@tailwindcss/line-clamp": "^0.4.2",
"@tloncorp/eslint-config": "^0.0.6",
"@types/lodash": "^4.14.172",
"@types/mousetrap": "^1.6.8",
Expand Down
76 changes: 70 additions & 6 deletions ui/public/mockServiceWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
/* tslint:disable */

/**
* Mock Service Worker (0.47.3).
* Mock Service Worker (0.43.1).
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
* - Please do NOT serve this file on production.
*/

const INTEGRITY_CHECKSUM = 'b3066ef78c2f9090b4ce87e874965995'
const INTEGRITY_CHECKSUM = 'c9450df6e4dc5e45740c3b0b640727a2'
const activeClientIds = new Set()

self.addEventListener('install', function () {
Expand Down Expand Up @@ -200,7 +200,7 @@ async function getResponse(event, client, requestId) {

function passthrough() {
// Clone the request because it might've been already used
// (i.e. its body has been read and sent to the client).
// (i.e. its body has been read and sent to the cilent).
const headers = Object.fromEntries(clonedRequest.headers.entries())

// Remove MSW-specific request headers so the bypassed requests
Expand Down Expand Up @@ -231,6 +231,13 @@ async function getResponse(event, client, requestId) {
return passthrough()
}

// Create a communication channel scoped to the current request.
// This way events can be exchanged outside of the worker's global
// "message" event listener (i.e. abstracted into functions).
const operationChannel = new BroadcastChannel(
`msw-response-stream-${requestId}`,
)

// Notify the client that a request has been intercepted.
const clientMessage = await sendToClient(client, {
type: 'REQUEST',
Expand All @@ -255,21 +262,43 @@ async function getResponse(event, client, requestId) {

switch (clientMessage.type) {
case 'MOCK_RESPONSE': {
return respondWithMock(clientMessage.data)
return respondWithMock(clientMessage.payload)
}

case 'MOCK_RESPONSE_START': {
return respondWithMockStream(operationChannel, clientMessage.payload)
}

case 'MOCK_NOT_FOUND': {
return passthrough()
}

case 'NETWORK_ERROR': {
const { name, message } = clientMessage.data
const { name, message } = clientMessage.payload
const networkError = new Error(message)
networkError.name = name

// Rejecting a "respondWith" promise emulates a network error.
throw networkError
}

case 'INTERNAL_ERROR': {
const parsedBody = JSON.parse(clientMessage.payload.body)

console.error(
`\
[MSW] Uncaught exception in the request handler for "%s %s":
${parsedBody.location}
This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error, as it indicates a mistake in your code. If you wish to mock an error response, please see this guide: https://mswjs.io/docs/recipes/mocking-error-responses\
`,
request.method,
request.url,
)

return respondWithMock(clientMessage.payload)
}
}

return passthrough()
Expand All @@ -287,7 +316,7 @@ function sendToClient(client, message) {
resolve(event.data)
}

client.postMessage(message, [channel.port2])
client.postMessage(JSON.stringify(message), [channel.port2])
})
}

Expand All @@ -301,3 +330,38 @@ async function respondWithMock(response) {
await sleep(response.delay)
return new Response(response.body, response)
}

function respondWithMockStream(operationChannel, mockResponse) {
let streamCtrl
const stream = new ReadableStream({
start: (controller) => (streamCtrl = controller),
})

return new Promise(async (resolve, reject) => {
operationChannel.onmessageerror = (event) => {
operationChannel.close()
return reject(event.data.error)
}

operationChannel.onmessage = (event) => {
if (!event.data) {
return
}

switch (event.data.type) {
case 'MOCK_RESPONSE_CHUNK': {
streamCtrl.enqueue(event.data.payload)
break
}

case 'MOCK_RESPONSE_END': {
streamCtrl.close()
operationChannel.close()
}
}
}

await sleep(mockResponse.delay)
return resolve(new Response(stream, mockResponse))
})
}
4 changes: 3 additions & 1 deletion ui/src/components/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ const sizeMap: Record<AvatarSizes, AvatarMeta> = {
default: { classes: 'w-12 h-12 rounded-lg', size: 24 },
};

const foregroundFromBackground = (background: string): 'black' | 'white' => {
export const foregroundFromBackground = (
background: string
): 'black' | 'white' => {
const rgb = {
r: parseInt(background.slice(1, 3), 16),
g: parseInt(background.slice(3, 5), 16),
Expand Down
66 changes: 66 additions & 0 deletions ui/src/components/GroupAvatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import cn from 'classnames';
import React, { useState } from 'react';
import ColorBoxIcon from '../components/icons/ColorBoxIcon';
import { isColor } from '../state/util';
import { useIsDark } from '../logic/useMedia';
import { useCalm } from '../state/settings';
import { useAvatar } from '../state/avatar';

interface GroupAvatarProps {
image?: string;
size?: string;
className?: string;
title?: string;
loadImage?: boolean;
}

const textSize = (size: string) => {
const dims = parseInt(size.replace(/[^0-9.]/g, ''), 10);
switch (dims) {
case 7272:
return 'text-3xl';
case 2020:
return 'text-xl';
case 1616:
return 'text-xl';
case 1414:
return 'text-xl';
case 1212:
return 'text-xl';
case 66:
return 'text-sm';
default:
return 'text-sm';
}
};

export default function GroupAvatar({
image,
size = 'h-6 w-6',
className,
title,
loadImage = true,
}: GroupAvatarProps) {
const { hasLoaded, load } = useAvatar(image || '');
const showImage = hasLoaded || loadImage;
const dark = useIsDark();
const calm = useCalm();
let background;
const symbols = [...(title || '')];

if (showImage && !calm.disableRemoteContent) {
background = image || (dark ? '#333333' : '#E5E5E5');
} else {
background = dark ? '#333333' : '#E5E5E5';
}

return image && showImage && !calm.disableRemoteContent && !isColor(image) ? (
<img className={cn('rounded', size, className)} src={image} onLoad={load} />
) : (
<ColorBoxIcon
className={cn('rounded', size, textSize(size), className)}
color={background}
letter={title ? symbols[0] : ''}
/>
);
}
40 changes: 29 additions & 11 deletions ui/src/components/ShipName.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
import { cite } from '@urbit/api';
import React, { HTMLAttributes } from 'react';
import { useCalm } from '../state/settings';
import { useContact } from '../state/contact';

type ShipNameProps = {
name: string;
truncate?: boolean;
showAlias?: boolean;
} & HTMLAttributes<HTMLSpanElement>;

export const ShipName = ({ name, truncate = true, ...props }: ShipNameProps) => {
export function ShipName({
name,
showAlias = false,
truncate = true,
...props
}: ShipNameProps) {
const contact = useContact(name);
const separator = /([_^-])/;
const citedName = truncate ? cite(name) : name;
const calm = useCalm();

if (!citedName) {
return null;
Expand All @@ -17,20 +27,28 @@ export const ShipName = ({ name, truncate = true, ...props }: ShipNameProps) =>
const parts = citedName.replace('~', '').split(separator);
const first = parts.shift();


return (
<span {...props}>
<span aria-hidden>~</span>
<span>{first}</span>
{parts.length > 1 && (
{contact?.nickname && !calm.disableNicknames && showAlias ? (
<span title={citedName}>{contact.nickname}</span>
) : (
<>
{parts.map((piece, index) => (
<span key={`${piece}-${index}`} aria-hidden={separator.test(piece)}>
{piece}
</span>
))}
<span aria-hidden>~</span>
<span>{first}</span>
{parts.length > 1 && (
<>
{parts.map((piece, index) => (
<span
key={`${piece}-${index}`}
aria-hidden={separator.test(piece)}
>
{piece}
</span>
))}
</>
)}
</>
)}
</span>
);
};
}
27 changes: 27 additions & 0 deletions ui/src/components/icons/ColorBoxIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import classNames from 'classnames';
import { IconProps } from './icon';
import { foregroundFromBackground } from '../Avatar';

interface ColorBoxIconProps extends IconProps {
color: string;
letter: string;
}

export default function ColorBoxIcon({
className,
color,
letter,
}: ColorBoxIconProps) {
return (
<div
className={classNames(
className,
'flex items-center justify-center rounded-md'
)}
style={{ backgroundColor: color }}
>
<span style={{ color: foregroundFromBackground(color) }}>{letter}</span>
</div>
);
}
4 changes: 4 additions & 0 deletions ui/src/logic/useMedia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ export const useMedia = (mediaQuery: string) => {

return match;
};

export function useIsDark() {
return useMedia('(prefers-color-scheme: dark)');
}
Loading

0 comments on commit 2678ec6

Please sign in to comment.