@@ -1,36 +1,51 @@
1−import React, { useMemo } from 'react';
2−import cn from 'classnames';
1+import React from 'react';
32import { Just, Nothing } from 'folktale/maybe';
43import ob from 'urbit-ob';
5−import { Grid, Flex, Text } from 'indigo-react';
6−import { times } from 'lodash';
4+import BN from 'bn.js';
75
86import * as need from 'lib/need';
9−import usePointBirthday from 'lib/usePointBirthday';
10−
11−import Blinky, {
12− LOADING_CHARACTER,
13− INTERSTITIAL_CHARACTER,
14− matchBlinkyDate,
15−} from './Blinky';
16−import Sigil from './Sigil';
17−import MaybeSigil from './MaybeSigil';
7+import { chunkStr, Matrix, walk, rand } from 'lib/card';
188import usePermissionsForPoint from 'lib/usePermissionsForPoint';
19−import { useWallet } from 'store/wallet';
209import { useSyncOwnedPoints } from 'lib/useSyncPoints';
2110
22−const TICKET_CHAR_LENGTH = 30;
23−const TICKET = times(TICKET_CHAR_LENGTH, i =>
24− i % 2 === 0 ? LOADING_CHARACTER : INTERSTITIAL_CHARACTER
25−).join('');
26−const PENDING_TICKET = TICKET;
27−const VALID_TICKET = `${TICKET} ◆`;
11+import { useWallet } from 'store/wallet';
2812
29−const VISIBLE_ADDRESS_CHAR_COUNT = 20;
30−const EMPTY_ADDRESS = times(VISIBLE_ADDRESS_CHAR_COUNT, () => '<').join('');
31−const ADDRESS_BUFFER_CHARS = times(40, () => '<').join('');
13+import MaybeSigil from './MaybeSigil';
14+import { sigil, reactRenderer } from 'urbit-sigil-js';
3215
33−const INVERTED_COLORWAY = ['#fff', '#000'];
16+function makeSigil(size, patp, colors) {
17+ const config = {
18+ patp: patp,
19+ colors: colors,
20+ margin: 0,
21+ renderer: reactRenderer,
22+ };
23+
24+ // Planet
25+ if (patp.length === 14) {
26+ return sigil({
27+ ...config,
28+ size: size,
29+ });
30+ }
31+ // Star
32+ if (patp.length === 7) {
33+ return sigil({
34+ ...config,
35+ width: size * 2,
36+ height: size,
37+ full: true,
38+ });
39+ }
40+ // Galaxy
41+ if (patp.length === 4) {
42+ return sigil({
43+ ...config,
44+ size: size,
45+ full: true,
46+ });
47+ }
48+}
3449
3550const buildKeyType = permissions => {
3651 if (permissions.isOwner) {
@@ -46,139 +61,256 @@ const buildKeyType = permissions => {
4661 }
4762};
4863
64+const symbols = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
65+
4966/**
50− * point is Maybe<number>
51− * ticket is null | boolean
52− * address is null | Maybe<string>
67+ * Address is Maybe<string>
68+ * point is Maybe<string>
69+ * animationMode is 'slide' | 'step' | 'none'
70+ *
5371 */
54−function Passport({
55− point,
56− className,
57− ticket = null,
58− address = Nothing(),
59− onClick,
60−}) {
61− const birthday = usePointBirthday(point.getOrElse(null));
62− const visualBirthday = matchBlinkyDate(birthday);
63−
64− const name = point.matchWith({
65− Nothing: () => Nothing(),
66− Just: p => Just(ob.patp(p.value)),
67− });
68− const visualName = name.matchWith({
69− Nothing: () => '...',
70− Just: p => p.value,
72+function Passport({ address, point, inverted, animationMode, keyType }) {
73+ const [cols, rows, tile] = [35, 12, 12];
74+
75+ const loading = Nothing.hasInstance(address) || Nothing.hasInstance(point);
76+ const defaultMatrix = () => {
77+ return Matrix.new(rows, cols);
78+ };
79+
80+ const patp = ob.patp(point.value);
81+
82+ const makeMatrix = addr => {
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));
101+
102+ return matrix;
103+ };
104+
105+ const matrix = address.matchWith({
106+ Just: ({ value }) => makeMatrix(value),
107+ Nothing: defaultMatrix,
71108 });
72109
73− const showTicket = ticket !== null;
74− const isValidTicket = !!ticket;
75− const visibleTicket = isValidTicket ? VALID_TICKET : PENDING_TICKET;
76− const visibleAddress = `${address.matchWith({
77− Nothing: () => EMPTY_ADDRESS,
78− Just: p => p.value.slice(0, VISIBLE_ADDRESS_CHAR_COUNT),
79− })}${ADDRESS_BUFFER_CHARS}`;
110+ const bgColor = inverted ? 'white' : 'black';
111+
112+ const fgColor = inverted ? 'black' : 'white';
80113
81114 return (
82− <Grid onClick={onClick} gap={4} className={cn('bg-black r8 p7', className)}>
83− <Grid.Item cols={[1, 4]}>
84− <MaybeSigil patp={name} size={64} />
85− </Grid.Item>
86− <Grid.Item as={Flex} cols={[4, 13]} col justify="between">
87− <Flex.Item className="mono white f5 pb2">{visualName}</Flex.Item>
88− <Flex.Item as={Flex} col>
89− <Flex.Item as={Text} className="mono f6 f5-ns gray4 uppercase">
90− Birth Time
91− </Flex.Item>
92− <Flex.Item as={Text} className="mono f6 f5-ns gray4 uppercase">
93− {visualBirthday}
94− </Flex.Item>
95− </Flex.Item>
96− </Grid.Item>
97− {showTicket && (
98− <Grid.Item full className="mv3" as={Flex} col>
99− <Flex.Item as={Text} className="mono f6 f5-ns gray4 uppercase">
100− Master Ticket
101− </Flex.Item>
102− <Flex.Item
103− as={Text}
104− className={cn('arial f6 f5-ns wrap', {
105− gray4: !isValidTicket,
106− green3: isValidTicket,
107− })}>
108− {visibleTicket}
109− </Flex.Item>
110− <Flex.Item as={Text} className="mono f6 f5-ns gray4 wrap">
111− {visibleAddress}
112− </Flex.Item>
113− </Grid.Item>
114− )}
115− </Grid>
115+ <div
116+ style={{
117+ fontFamily: 'Inter',
118+ backgroundColor: bgColor,
119+ border: inverted ? '2px solid #E6E6E6' : '2px solid black',
120+ borderRadius: '20px',
121+ marginBottom: '16px',
122+ minWidth: `${16 + 16 + cols * tile}px`,
123+ display: 'flex',
124+ flexDirection: 'column',
125+ }}>
126+ <div
127+ style={{
128+ height: '64px',
129+ padding: '16px',
130+ display: 'flex',
131+ }}>
132+ <div
133+ style={{
134+ width: '100%',
135+ display: 'flex',
136+ alignItems: 'center',
137+ }}>
138+ {Just.hasInstance(point) && (
139+ <MaybeSigil patp={patp.map(ob.patp).value} />
140+ )}
141+ <div
142+ style={{
143+ marginLeft: '16px',
144+ }}>
145+ <div
146+ style={{
147+ fontWeight: '600',
148+ color: fgColor,
149+ fontSize: '16px',
150+ fontFamily: 'Source Code Pro',
151+ }}>
152+ {patp}
153+ </div>
154+ {keyType !== '' ? (
155+ <div
156+ style={{
157+ marginTop: '8px',
158+ fontWeight: '500',
159+ color: fgColor,
160+ fontSize: '14px',
161+ fontFamily: 'Source Code Pro',
162+ }}>
163+ {keyType}
164+ </div>
165+ ) : (
166+ <div />
167+ )}
168+ </div>
169+ </div>
170+ </div>
171+ <div
172+ style={{
173+ display: 'flex',
174+ flexDirection: 'column',
175+ padding: '16px',
176+ paddingTop: '0px',
177+ filter: inverted ? 'invert(1)' : '',
178+ }}>
179+ {matrix.map((row, _row) => {
180+ return (
181+ <div
182+ key={`card:row:${_row}`}
183+ style={{ display: 'flex', justifyContent: 'space-between' }}>
184+ {row.map((v, _col) => {
185+ return (
186+ <Cell
187+ row={_row}
188+ col={_col}
189+ value={typeof symbols[v] === 'undefined' ? 13 : v}
190+ spriteSheet={'a'}
191+ spriteSheetHeight={12}
192+ spriteSheetWidth={168}
193+ inverted={inverted}
194+ loading={loading}
195+ animationMode={animationMode}
196+ />
197+ );
198+ })}
199+ </div>
200+ );
201+ })}
202+ </div>
203+ </div>
116204 );
117205}
118206
119−// TODO: Passport.Mini should probably accept Maybe<> for point
120−// in order to display a pretty skeleton loading view
121−// but that requires making usePermissionsForPoint accept a Maybe<point>
122−// and I don't feel like that refactor is worth it right now, and the spec
123−// doesn't call for it, hence the TODO
124−Passport.Mini = function MiniPassport({ point, className, inverted, ...rest }) {
125− useSyncOwnedPoints([point]);
207+const Cell = props => {
208+ return (
209+ <div
210+ key={`card:cell:${props.row}:${props.col}:${props.value}`}
211+ className={
212+ props.loading
213+ ? 'blink'
214+ : props.animationMode === 'none'
215+ ? ''
216+ : `${props.animationMode}-h-${props.value}`
217+ }
218+ style={{
219+ backgroundImage: `url(/spritesheet_a.png)`,
220+ backgroundPosition:
221+ props.animationMode === 'none'
222+ ? `-${props.spriteSheetHeight * props.value}px 0px`
223+ : '',
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`,
230+ }}
231+ />
232+ );
233+};
126234
235+/**
236+ * point is number
237+ *
238+ */
239+function MiniPassport({ point, inverted, ...rest }) {
240+ useSyncOwnedPoints([point]);
127241 const { wallet } = useWallet();
128242 const address = need.addressFromWallet(wallet);
129243
244+ const patp = ob.patp(point);
130245 const permissions = usePermissionsForPoint(address, point);
131246 const keyType = buildKeyType(permissions);
132− const name = useMemo(() => ob.patp(point), [point]);
133−
134247 return (
135− <Grid
136− gap={4}
137− className={cn(
138− 'r8 p4',
139− {
140− 'bg-black': !inverted,
141− 'bg-white b-gray4 b1': inverted,
142− },
143− className
144− )}
248+ <div
249+ style={{
250+ fontFamily: 'Inter',
251+ backgroundColor: inverted ? 'white' : 'black',
252+ border: inverted ? '2px solid #E6E6E6' : '2px solid black',
253+ borderRadius: '20px',
254+ marginBottom: '16px',
255+ width: `${230}px`,
256+ display: 'flex',
257+ flexDirection: 'column',
258+ }}
145259 {...rest}>
146− <Grid.Item cols={[1, 4]}>
147− <Sigil
148− patp={name}
149− size={64}
150− colors={inverted ? INVERTED_COLORWAY : undefined}
151− />
152− </Grid.Item>
153− <Grid.Item as={Flex} cols={[4, 13]} col justify="between">
154− <Flex.Item
155− className={cn('mono f6 pb2', {
156− white: !inverted,
157− black: inverted,
158− })}>
159− {name}
160− </Flex.Item>
161− <Flex.Item as={Flex} col className={cn('mono f6 uppercase')}>
162− <Flex.Item
163− as={Text}
164− className={cn({
165− gray4: !inverted,
166− black: inverted,
167− })}>
168− Key Type
169− </Flex.Item>
170− <Flex.Item
171− as={Text}
172− className={cn({
173− white: !inverted,
174− black: inverted,
175− })}>
176− {keyType || <Blinky />}
177− </Flex.Item>
178− </Flex.Item>
179− </Grid.Item>
180− </Grid>
260+ <div
261+ style={{
262+ height: '44px',
263+ padding: '16px',
264+ display: 'flex',
265+ }}>
266+ <div
267+ style={{
268+ width: '100%',
269+ display: 'flex',
270+ alignItems: 'center',
271+ }}>
272+ {makeSigil(
273+ 44,
274+ patp,
275+ inverted ? ['white', 'black'] : ['black', 'white']
276+ )}
277+ <div
278+ style={{
279+ marginLeft: '16px',
280+ display: 'flex',
281+ flexDirection: 'column',
282+ justifyContent: 'space-between',
283+ }}>
284+ <div
285+ style={{
286+ fontWeight: '600',
287+ color: inverted ? 'black' : 'white',
288+ fontSize: '14px',
289+ fontFamily: 'Source Code Pro',
290+ }}>
291+ {patp}
292+ </div>
293+ {keyType !== '' ? (
294+ <div
295+ style={{
296+ marginTop: '8px',
297+ fontWeight: '500',
298+ color: inverted ? 'black' : 'white',
299+ fontSize: '12px',
300+ fontFamily: 'Source Code Pro',
301+ }}>
302+ {keyType}
303+ </div>
304+ ) : (
305+ <div />
306+ )}
307+ </div>
308+ </div>
309+ </div>
310+ </div>
181311 );
182−};
312+}
313+
314+Passport.Mini = MiniPassport;
183315
184316export default Passport;