@@ -1,36 +1,51 @@
1−import React, { useMemo } from 'react';
1+import React from 'react';
2−import cn from 'classnames';
3import { Just, Nothing } from 'folktale/maybe';
2import { Just, Nothing } from 'folktale/maybe';
4import ob from 'urbit-ob';
3import ob from 'urbit-ob';
5−import { Grid, Flex, Text } from 'indigo-react';
4+import BN from 'bn.js';
6−import { times } from 'lodash';
8import * as need from 'lib/need';
6import * as need from 'lib/need';
9−import usePointBirthday from 'lib/usePointBirthday';
7+import { chunkStr, Matrix, walk, rand } from 'lib/card';
13− INTERSTITIAL_CHARACTER,
16−import Sigil from './Sigil';
17−import MaybeSigil from './MaybeSigil';
18import usePermissionsForPoint from 'lib/usePermissionsForPoint';
8import usePermissionsForPoint from 'lib/usePermissionsForPoint';
19−import { useWallet } from 'store/wallet';
20import { useSyncOwnedPoints } from 'lib/useSyncPoints';
9import { useSyncOwnedPoints } from 'lib/useSyncPoints';
22−const TICKET_CHAR_LENGTH = 30;
11+import { useWallet } from 'store/wallet';
23−const TICKET = times(TICKET_CHAR_LENGTH, i =>
24− i % 2 === 0 ? LOADING_CHARACTER : INTERSTITIAL_CHARACTER
26−const PENDING_TICKET = TICKET;
27−const VALID_TICKET = `${TICKET} ◆`;
29−const VISIBLE_ADDRESS_CHAR_COUNT = 20;
13+import MaybeSigil from './MaybeSigil';
30−const EMPTY_ADDRESS = times(VISIBLE_ADDRESS_CHAR_COUNT, () => '<').join('');
14+import { sigil, reactRenderer } from 'urbit-sigil-js';
31−const ADDRESS_BUFFER_CHARS = times(40, () => '<').join('');
33−const INVERTED_COLORWAY = ['#fff', '#000'];
16+function makeSigil(size, patp, colors) {
21+ renderer: reactRenderer,
25+ if (patp.length === 14) {
32+ if (patp.length === 7) {
41+ if (patp.length === 4) {
35const buildKeyType = permissions => {
50const buildKeyType = permissions => {
36 if (permissions.isOwner) {
51 if (permissions.isOwner) {
@@ -46,139 +61,256 @@ const buildKeyType = permissions => {
64+const symbols = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
67+ * Address is Maybe<string>
50− * point is Maybe<number>
68+ * point is Maybe<string>
51− * ticket is null | boolean
69+ * animationMode is 'slide' | 'step' | 'none'
52− * address is null | Maybe<string>
70+ *
54−function Passport({
72+function Passport({ address, point, inverted, animationMode, keyType }) {
55− point,
73+ const [cols, rows, tile] = [35, 12, 12];
57− ticket = null,
75+ const loading = Nothing.hasInstance(address) || Nothing.hasInstance(point);
58− address = Nothing(),
76+ const defaultMatrix = () => {
59− onClick,
77+ return Matrix.new(rows, cols);
61− const birthday = usePointBirthday(point.getOrElse(null));
79+
62− const visualBirthday = matchBlinkyDate(birthday);
64− const name = point.matchWith({
65− Nothing: () => Nothing(),
66− Just: p => Just(ob.patp(p.value)),
80+ const patp = ob.patp(point.value);
68− const visualName = name.matchWith({
82+ const makeMatrix = addr => {
69− Nothing: () => '...',
83+ // remove the 0x
84+ const onlyTheNumberPart = addr.substring(2);
85+ // make bigNum from hex
86+ const int = new BN(onlyTheNumberPart, 16);
87+ // make a bigger number
88+ const big = int.pow(new BN(10));
89+ // parse that to a string and pad
90+ const b10 = big.toString(10).padStart(32, '0');
91+ // split the big number into parts
92+ const chunkSize = Math.ceil(b10.length / 8);
93+ const chunks = chunkStr(b10, chunkSize).map(c => new BN(c));
94+ // generate an empty matrix
95+ const startMatrix = Matrix.new(rows, cols);
96+ // do a few random walks starting at diff coordinates
97+ const [, matrix] = chunks.reduce((acc, chunk) => {
98+ return walk(chunk, acc[1]);
99+ // start with the 'random' points (not actually random, they are deterministic)
100+ }, rand(big, startMatrix));
105+ const matrix = address.matchWith({
70− Just: p => p.value,
106+ Just: ({ value }) => makeMatrix(value),
107+ Nothing: defaultMatrix,
73− const showTicket = ticket !== null;
110+ const bgColor = inverted ? 'white' : 'black';
74− const isValidTicket = !!ticket;
111+
75− const visibleTicket = isValidTicket ? VALID_TICKET : PENDING_TICKET;
112+ const fgColor = inverted ? 'black' : 'white';
76− const visibleAddress = `${address.matchWith({
77− Nothing: () => EMPTY_ADDRESS,
78− Just: p => p.value.slice(0, VISIBLE_ADDRESS_CHAR_COUNT),
79− })}${ADDRESS_BUFFER_CHARS}`;
82− <Grid onClick={onClick} gap={4} className={cn('bg-black r8 p7', className)}>
115+ <div
83− <Grid.Item cols={[1, 4]}>
116+ style={{
84− <MaybeSigil patp={name} size={64} />
117+ fontFamily: 'Inter',
85− </Grid.Item>
118+ backgroundColor: bgColor,
86− <Grid.Item as={Flex} cols={[4, 13]} col justify="between">
119+ border: inverted ? '2px solid #E6E6E6' : '2px solid black',
87− <Flex.Item className="mono white f5 pb2">{visualName}</Flex.Item>
120+ borderRadius: '20px',
88− <Flex.Item as={Flex} col>
121+ marginBottom: '16px',
89− <Flex.Item as={Text} className="mono f6 f5-ns gray4 uppercase">
122+ minWidth: `${16 + 16 + cols * tile}px`,
90− Birth Time
123+ display: 'flex',
91− </Flex.Item>
124+ flexDirection: 'column',
92− <Flex.Item as={Text} className="mono f6 f5-ns gray4 uppercase">
125+ }}>
93− {visualBirthday}
126+ <div
94− </Flex.Item>
127+ style={{
95− </Flex.Item>
128+ height: '64px',
96− </Grid.Item>
129+ padding: '16px',
97− {showTicket && (
130+ display: 'flex',
98− <Grid.Item full className="mv3" as={Flex} col>
131+ }}>
99− <Flex.Item as={Text} className="mono f6 f5-ns gray4 uppercase">
132+ <div
100− Master Ticket
133+ style={{
101− </Flex.Item>
134+ width: '100%',
102− <Flex.Item
135+ display: 'flex',
103− as={Text}
136+ alignItems: 'center',
104− className={cn('arial f6 f5-ns wrap', {
137+ }}>
105− gray4: !isValidTicket,
138+ {Just.hasInstance(point) && (
106− green3: isValidTicket,
139+ <MaybeSigil patp={patp.map(ob.patp).value} />
150+ fontFamily: 'Source Code Pro',
161+ fontFamily: 'Source Code Pro',
174+ flexDirection: 'column',
177+ filter: inverted ? 'invert(1)' : '',
179+ {matrix.map((row, _row) => {
182+ key={`card:row:${_row}`}
183+ style={{ display: 'flex', justifyContent: 'space-between' }}>
184+ {row.map((v, _col) => {
189+ value={typeof symbols[v] === 'undefined' ? 13 : v}
191+ spriteSheetHeight={12}
192+ spriteSheetWidth={168}
195+ animationMode={animationMode}
108− {visibleTicket}
199+ </div>
110− <Flex.Item as={Text} className="mono f6 f5-ns gray4 wrap">
201+ })}
111− {visibleAddress}
202+ </div>
112− </Flex.Item>
203+ </div>
119−// TODO: Passport.Mini should probably accept Maybe<> for point
207+const Cell = props => {
120−// in order to display a pretty skeleton loading view
208+ return (
121−// but that requires making usePermissionsForPoint accept a Maybe<point>
209+ <div
122−// and I don't feel like that refactor is worth it right now, and the spec
210+ key={`card:cell:${props.row}:${props.col}:${props.value}`}
123−// doesn't call for it, hence the TODO
211+ className={
124−Passport.Mini = function MiniPassport({ point, className, inverted, ...rest }) {
212+ props.loading
125− useSyncOwnedPoints([point]);
213+ ? 'blink'
214+ : props.animationMode === 'none'
216+ : `${props.animationMode}-h-${props.value}`
219+ backgroundImage: `url(/spritesheet_a.png)`,
221+ props.animationMode === 'none'
222+ ? `-${props.spriteSheetHeight * props.value}px 0px`
224+ backgroundSize: `${props.spriteSheetWidth}px ${props.spriteSheetHeight}px`,
225+ width: `${props.spriteSheetHeight}px`,
226+ maxWidth: `${props.spriteSheetHeight}px`,
227+ height: `${props.spriteSheetHeight}px`,
228+ maxHeight: `${props.spriteSheetHeight}px`,
229+ animationDelay: `${props.value * 50}ms`,
239+function MiniPassport({ point, inverted, ...rest }) {
240+ useSyncOwnedPoints([point]);
127 const { wallet } = useWallet();
241 const { wallet } = useWallet();
128 const address = need.addressFromWallet(wallet);
242 const address = need.addressFromWallet(wallet);
244+ const patp = ob.patp(point);
130 const permissions = usePermissionsForPoint(address, point);
245 const permissions = usePermissionsForPoint(address, point);
131 const keyType = buildKeyType(permissions);
246 const keyType = buildKeyType(permissions);
132− const name = useMemo(() => ob.patp(point), [point]);
136− gap={4}
249+ style={{
137− className={cn(
250+ fontFamily: 'Inter',
138− 'r8 p4',
251+ backgroundColor: inverted ? 'white' : 'black',
139− {
252+ border: inverted ? '2px solid #E6E6E6' : '2px solid black',
140− 'bg-black': !inverted,
253+ borderRadius: '20px',
141− 'bg-white b-gray4 b1': inverted,
254+ marginBottom: '16px',
142− },
255+ width: `${230}px`,
143− className
256+ display: 'flex',
257+ flexDirection: 'column',
145 {...rest}>
259 {...rest}>
146− <Grid.Item cols={[1, 4]}>
260+ <div
148− patp={name}
262+ height: '44px',
149− size={64}
263+ padding: '16px',
150− colors={inverted ? INVERTED_COLORWAY : undefined}
264+ display: 'flex',
152− </Grid.Item>
266+ <div
153− <Grid.Item as={Flex} cols={[4, 13]} col justify="between">
267+ style={{
154− <Flex.Item
268+ width: '100%',
155− className={cn('mono f6 pb2', {
269+ display: 'flex',
156− white: !inverted,
270+ alignItems: 'center',
157− black: inverted,
271+ }}>
275+ inverted ? ['white', 'black'] : ['black', 'white']
281+ flexDirection: 'column',
282+ justifyContent: 'space-between',
287+ color: inverted ? 'black' : 'white',
289+ fontFamily: 'Source Code Pro',
160− </Flex.Item>
292+ </div>
161− <Flex.Item as={Flex} col className={cn('mono f6 uppercase')}>
293+ {keyType !== '' ? (
163− as={Text}
295+ style={{
164− className={cn({
296+ marginTop: '8px',
165− gray4: !inverted,
297+ fontWeight: '500',
166− black: inverted,
298+ color: inverted ? 'black' : 'white',
167− })}>
299+ fontSize: '12px',
168− Key Type
300+ fontFamily: 'Source Code Pro',
176− {keyType || <Blinky />}
302+ {keyType}
177− </Flex.Item>
303+ </div>
178− </Flex.Item>
304+ ) : (
179− </Grid.Item>
305+ <div />
314+Passport.Mini = MiniPassport;
184export default Passport;
316export default Passport;