mirror of
https://github.com/urbit/shrub.git
synced 2024-12-01 06:35:32 +03:00
Merge branch 'lf/settings-screen' (#3236)
* origin/lf/settings-screen: (25 commits) interface: fix setting background color interface/api: fix s3 calls for setting endpoint, keys interface/profile: hide log out of all header interface/profile: hide 'log out of all' profile: address comments profile: add identity form publish,links: respect hide avatar and nickname settings StatusBar: update for new design chat: support hideAvatars and hideNicknames launch: add background to base hash StatusBar: update for profile + omnibox interface: breakpoints in px settings: refactor for line limit settings: add inline image upload settings: add drag and drop component to reorder tiles groups,dojo: fix transparencies clock: fix transparency on no location profile: refactor layout interface: change indigo breakpoints to old queries interface: handle transparency correctly ... Signed-off-by: Matilde Park <matilde.park@sunshinegardens.org>
This commit is contained in:
commit
dd82f3861a
@ -24,6 +24,5 @@
|
||||
<script src="/~landscape/js/channel.js"></script>
|
||||
<script src="/~landscape/js/session.js"></script>
|
||||
<script src="/~landscape/js/bundle/index.f58fbbc4b037bb976a2a.js"></script>
|
||||
<script src="https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
312
pkg/interface/package-lock.json
generated
312
pkg/interface/package-lock.json
generated
@ -1376,21 +1376,35 @@
|
||||
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="
|
||||
},
|
||||
"@reach/auto-id": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@reach/auto-id/-/auto-id-0.10.1.tgz",
|
||||
"integrity": "sha512-xGFW2v+L39M/mafdW7v+NhhsjT1LBnQJCGj64dm37T4IGNgAexlfMkRRwsqHOvuVvV38mR114YOy0xrlkqduRQ==",
|
||||
"version": "0.10.5",
|
||||
"resolved": "https://registry.npmjs.org/@reach/auto-id/-/auto-id-0.10.5.tgz",
|
||||
"integrity": "sha512-we4/bwjFxJ3F+2eaddQ1HltbKvJ7AB8clkN719El7Zugpn/vOjfPMOVUiBqTmPGLUvkYrq4tpuFwLvk2HyOVHg==",
|
||||
"requires": {
|
||||
"@reach/utils": "^0.10.1",
|
||||
"tslib": "^1.11.1"
|
||||
"@reach/utils": "0.10.5",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz",
|
||||
"integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@reach/descendants": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@reach/descendants/-/descendants-0.10.1.tgz",
|
||||
"integrity": "sha512-Wh6VnCCDwqK/07GBx259fQsVGGwb+IT17GP3LYPtabo2L/t9Mw5oIiAkXZ6VVvw7zGpQGfm9cZYBxdYCbQOwuA==",
|
||||
"version": "0.10.5",
|
||||
"resolved": "https://registry.npmjs.org/@reach/descendants/-/descendants-0.10.5.tgz",
|
||||
"integrity": "sha512-8HhN4DwS/HsPQ+Ym/Ft/XJ1spXBYdE8hqpnbYR9UcU7Nx3oDbTIdhjA6JXXt23t5avYIx2jRa8YHCtVKSHuiwA==",
|
||||
"requires": {
|
||||
"@reach/utils": "^0.10.1",
|
||||
"tslib": "^1.11.1"
|
||||
"@reach/utils": "0.10.5",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz",
|
||||
"integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@reach/disclosure": {
|
||||
@ -1430,53 +1444,81 @@
|
||||
}
|
||||
},
|
||||
"@reach/menu-button": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@reach/menu-button/-/menu-button-0.10.1.tgz",
|
||||
"integrity": "sha512-GqROR7McvLdNdLe70a7aNSZaRmqttSqGdnOVkLs4NiihX1FFOw/k5CCTWmN6WEKLayVV/r4WaP/lUDdMa8w7nA==",
|
||||
"version": "0.10.5",
|
||||
"resolved": "https://registry.npmjs.org/@reach/menu-button/-/menu-button-0.10.5.tgz",
|
||||
"integrity": "sha512-PQzFzexk9K7Q5qTGmXcg3qYp+F36H0MaeyzybR5t4lB1e56nAh1u/C2bocwpHssIoy25xOR8Nu+LVMVf6k6cUw==",
|
||||
"requires": {
|
||||
"@reach/auto-id": "^0.10.1",
|
||||
"@reach/descendants": "^0.10.1",
|
||||
"@reach/popover": "^0.10.1",
|
||||
"@reach/utils": "^0.10.1",
|
||||
"@reach/auto-id": "0.10.5",
|
||||
"@reach/descendants": "0.10.5",
|
||||
"@reach/popover": "0.10.5",
|
||||
"@reach/utils": "0.10.5",
|
||||
"prop-types": "^15.7.2",
|
||||
"tslib": "^1.11.1"
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz",
|
||||
"integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@reach/observe-rect": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@reach/observe-rect/-/observe-rect-1.1.0.tgz",
|
||||
"integrity": "sha512-kE+jvoj/OyJV24C03VvLt5zclb9ArJi04wWXMMFwQvdZjdHoBlN4g0ZQFjyy/ejPF1Z/dpUD5dhRdBiUmIGZTA=="
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@reach/observe-rect/-/observe-rect-1.2.0.tgz",
|
||||
"integrity": "sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ=="
|
||||
},
|
||||
"@reach/popover": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@reach/popover/-/popover-0.10.1.tgz",
|
||||
"integrity": "sha512-CDRYWnCUfvn2WlTDVlDmWOV3TD0zYeJSfsd6daq2bqUX1+1jRddm3x/nk2Na6Fn8Nm9pjYUvatE+noin9iVvDw==",
|
||||
"version": "0.10.5",
|
||||
"resolved": "https://registry.npmjs.org/@reach/popover/-/popover-0.10.5.tgz",
|
||||
"integrity": "sha512-S+qWIsjrN1yMpHjgELhjpdGc4Q3q1plJtXBGGQRxUAjmCUA/5OY7t5w5C8iqMNAEBwCvYXKvK/pLcXFxxLykSw==",
|
||||
"requires": {
|
||||
"@reach/portal": "^0.10.1",
|
||||
"@reach/rect": "^0.10.1",
|
||||
"@reach/utils": "^0.10.1",
|
||||
"@reach/portal": "0.10.5",
|
||||
"@reach/rect": "0.10.5",
|
||||
"@reach/utils": "0.10.5",
|
||||
"tabbable": "^4.0.0",
|
||||
"tslib": "^1.11.1"
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz",
|
||||
"integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@reach/portal": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@reach/portal/-/portal-0.10.1.tgz",
|
||||
"integrity": "sha512-axap4IxA0xgsxluqyeyVuGZrStqaZ81iyiHmXFn+D+bjDNdd29colHm5GEB5mjGnkqktcXWyx5DQ+aRHIyGEkQ==",
|
||||
"version": "0.10.5",
|
||||
"resolved": "https://registry.npmjs.org/@reach/portal/-/portal-0.10.5.tgz",
|
||||
"integrity": "sha512-K5K8gW99yqDPDCWQjEfSNZAbGOQWSx5AN2lpuR1gDVoz4xyWpTJ0k0LbetYJTDVvLP/InEcR7AU42JaDYDCXQw==",
|
||||
"requires": {
|
||||
"@reach/utils": "^0.10.1",
|
||||
"tslib": "^1.11.1"
|
||||
"@reach/utils": "0.10.5",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz",
|
||||
"integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@reach/rect": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@reach/rect/-/rect-0.10.1.tgz",
|
||||
"integrity": "sha512-jM172ZMUpdv4WeMjdO+A9Yg5doXWCq8SzRgk7Q7dK9x1y4czOmY0zanwYxDVs83r+mn0+QINnEDNcScpsOPAfQ==",
|
||||
"version": "0.10.5",
|
||||
"resolved": "https://registry.npmjs.org/@reach/rect/-/rect-0.10.5.tgz",
|
||||
"integrity": "sha512-JBKs2HniYecq5zLO6UFReX28SUBPM3n0aizdNgHuvwZmDcTfNV4jsuJYQLqJ+FbCQsrSHkBxKZqWpfGXY9bUEg==",
|
||||
"requires": {
|
||||
"@reach/observe-rect": "^1.1.0",
|
||||
"@reach/utils": "^0.10.1",
|
||||
"@reach/observe-rect": "1.2.0",
|
||||
"@reach/utils": "0.10.5",
|
||||
"prop-types": "^15.7.2",
|
||||
"tslib": "^1.11.1"
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz",
|
||||
"integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@reach/tabs": {
|
||||
@ -1527,15 +1569,38 @@
|
||||
}
|
||||
},
|
||||
"@reach/utils": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@reach/utils/-/utils-0.10.1.tgz",
|
||||
"integrity": "sha512-YzwZWVK+rSiUATNVtK7H2/ZkT/GhNKmkRjnj3hnVhSYLGxY9uQdfc+npetOqkh4hTAOXiErDa64ybVClR3h0TA==",
|
||||
"version": "0.10.5",
|
||||
"resolved": "https://registry.npmjs.org/@reach/utils/-/utils-0.10.5.tgz",
|
||||
"integrity": "sha512-5E/xxQnUbmpI/LrufBAOXjunl96DnqX6B4zC2MO2KH/dRzLug5gM5VuOwV26egsp0jvsSPxojwciOhS43px3qw==",
|
||||
"requires": {
|
||||
"@types/warning": "^3.0.0",
|
||||
"tslib": "^1.11.1",
|
||||
"tslib": "^2.0.0",
|
||||
"warning": "^4.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz",
|
||||
"integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@react-dnd/asap": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.0.tgz",
|
||||
"integrity": "sha512-0XhqJSc6pPoNnf8DhdsPHtUhRzZALVzYMTzRwV4VI6DJNJ/5xxfL9OQUwb8IH5/2x7lSf7nAZrnzUD+16VyOVQ=="
|
||||
},
|
||||
"@react-dnd/invariant": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-2.0.0.tgz",
|
||||
"integrity": "sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw=="
|
||||
},
|
||||
"@react-dnd/shallowequal": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz",
|
||||
"integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==",
|
||||
"dev": true
|
||||
},
|
||||
"@styled-system/background": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@styled-system/background/-/background-5.1.2.tgz",
|
||||
@ -2382,6 +2447,48 @@
|
||||
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
|
||||
"dev": true
|
||||
},
|
||||
"aws-sdk": {
|
||||
"version": "2.726.0",
|
||||
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.726.0.tgz",
|
||||
"integrity": "sha512-QRQ7MaW5dprdr/T3vCTC+J8TeUfpM45yWsBuATPcCV/oO8afFHVySwygvGLY4oJuo5Mf4mJn3+JYTquo6CqiaA==",
|
||||
"requires": {
|
||||
"buffer": "4.9.2",
|
||||
"events": "1.1.1",
|
||||
"ieee754": "1.1.13",
|
||||
"jmespath": "0.15.0",
|
||||
"querystring": "0.2.0",
|
||||
"sax": "1.2.1",
|
||||
"url": "0.10.3",
|
||||
"uuid": "3.3.2",
|
||||
"xml2js": "0.4.19"
|
||||
},
|
||||
"dependencies": {
|
||||
"events": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
|
||||
"integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ="
|
||||
},
|
||||
"punycode": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
|
||||
"integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
|
||||
},
|
||||
"url": {
|
||||
"version": "0.10.3",
|
||||
"resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz",
|
||||
"integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=",
|
||||
"requires": {
|
||||
"punycode": "1.3.2",
|
||||
"querystring": "0.2.0"
|
||||
}
|
||||
},
|
||||
"uuid": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
|
||||
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"babel-eslint": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz",
|
||||
@ -2518,8 +2625,7 @@
|
||||
"base64-js": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
|
||||
"integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==",
|
||||
"dev": true
|
||||
"integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
|
||||
},
|
||||
"batch": {
|
||||
"version": "0.6.1",
|
||||
@ -2741,7 +2847,6 @@
|
||||
"version": "4.9.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz",
|
||||
"integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"base64-js": "^1.0.2",
|
||||
"ieee754": "^1.1.4",
|
||||
@ -2751,8 +2856,7 @@
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
|
||||
"dev": true
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -3167,9 +3271,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"codemirror": {
|
||||
"version": "5.53.2",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.53.2.tgz",
|
||||
"integrity": "sha512-wvSQKS4E+P8Fxn/AQ+tQtJnF1qH5UOlxtugFLpubEZ5jcdH2iXTVinb+Xc/4QjshuOxRm4fUsU2QPF1JJKiyXA=="
|
||||
"version": "5.57.0",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.57.0.tgz",
|
||||
"integrity": "sha512-WGc6UL7Hqt+8a6ZAsj/f1ApQl3NPvHY/UQSzG6fB6l4BjExgVdhFaxd7mRTw1UCiYe/6q86zHP+kfvBQcZGvUg=="
|
||||
},
|
||||
"collapse-white-space": {
|
||||
"version": "1.0.6",
|
||||
@ -3756,6 +3860,21 @@
|
||||
"randombytes": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"dnd-core": {
|
||||
"version": "11.1.3",
|
||||
"resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-11.1.3.tgz",
|
||||
"integrity": "sha512-QugF55dNW+h+vzxVJ/LSJeTeUw9MCJ2cllhmVThVPEtF16ooBkxj0WBE5RB+AceFxMFo1rO6bJKXtqKl+JNnyA==",
|
||||
"requires": {
|
||||
"@react-dnd/asap": "^4.0.0",
|
||||
"@react-dnd/invariant": "^2.0.0",
|
||||
"redux": "^4.0.4"
|
||||
}
|
||||
},
|
||||
"dnd-multi-backend": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dnd-multi-backend/-/dnd-multi-backend-6.0.0.tgz",
|
||||
"integrity": "sha512-qfUO4V0IACs24xfE9m9OUnwIzoL+SWzSiFbKVIHE0pFddJeZ93BZOdHS1XEYr8X3HNh+CfnfjezXgOMgjvh74g=="
|
||||
},
|
||||
"dns-equal": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz",
|
||||
@ -5519,8 +5638,7 @@
|
||||
"ieee754": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
|
||||
"integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
|
||||
},
|
||||
"iferr": {
|
||||
"version": "0.1.5",
|
||||
@ -5976,6 +6094,11 @@
|
||||
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
|
||||
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8="
|
||||
},
|
||||
"jmespath": {
|
||||
"version": "0.15.0",
|
||||
"resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz",
|
||||
"integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc="
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@ -7593,8 +7716,7 @@
|
||||
"querystring": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
|
||||
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=",
|
||||
"dev": true
|
||||
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
|
||||
},
|
||||
"querystring-es3": {
|
||||
"version": "0.2.1",
|
||||
@ -7673,6 +7795,53 @@
|
||||
"resolved": "https://registry.npmjs.org/react-codemirror2/-/react-codemirror2-6.0.1.tgz",
|
||||
"integrity": "sha512-rutEKVgvFhWcy/GeVA1hFbqrO89qLqgqdhUr7YhYgIzdyICdlRQv+ztuNvOFQMXrO0fLt0VkaYOdMdYdQgsSUA=="
|
||||
},
|
||||
"react-dnd": {
|
||||
"version": "11.1.3",
|
||||
"resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-11.1.3.tgz",
|
||||
"integrity": "sha512-8rtzzT8iwHgdSC89VktwhqdKKtfXaAyC4wiqp0SywpHG12TTLvfOoL6xNEIUWXwIEWu+CFfDn4GZJyynCEuHIQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@react-dnd/shallowequal": "^2.0.0",
|
||||
"@types/hoist-non-react-statics": "^3.3.1",
|
||||
"dnd-core": "^11.1.3",
|
||||
"hoist-non-react-statics": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"react-dnd-html5-backend": {
|
||||
"version": "11.1.3",
|
||||
"resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-11.1.3.tgz",
|
||||
"integrity": "sha512-/1FjNlJbW/ivkUxlxQd7o3trA5DE33QiRZgxent3zKme8DwF4Nbw3OFVhTRFGaYhHFNL1rZt6Rdj1D78BjnNLw==",
|
||||
"requires": {
|
||||
"dnd-core": "^11.1.3"
|
||||
}
|
||||
},
|
||||
"react-dnd-multi-backend": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-dnd-multi-backend/-/react-dnd-multi-backend-6.0.2.tgz",
|
||||
"integrity": "sha512-SwpqRv0HkJYu244FbHf9NbvGzGy14Ir9wIAhm909uvOVaHgsOq6I1THMSWSgpwUI31J3Bo5uS19tuvGpVPjzZw==",
|
||||
"requires": {
|
||||
"dnd-multi-backend": "^6.0.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-dnd-preview": "^6.0.2"
|
||||
}
|
||||
},
|
||||
"react-dnd-preview": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-dnd-preview/-/react-dnd-preview-6.0.2.tgz",
|
||||
"integrity": "sha512-F2+uK4Be+q+7mZfNh9kaZols7wp1hX6G7UBTVaTpDsBpMhjFvY7/v7odxYSerSFBShh23MJl33a4XOVRFj1zoQ==",
|
||||
"requires": {
|
||||
"prop-types": "^15.7.2"
|
||||
}
|
||||
},
|
||||
"react-dnd-touch-backend": {
|
||||
"version": "11.1.3",
|
||||
"resolved": "https://registry.npmjs.org/react-dnd-touch-backend/-/react-dnd-touch-backend-11.1.3.tgz",
|
||||
"integrity": "sha512-8lz4fxfYwUuJ6Y2seQYwh8+OfwKcbBX0CIbz7AwXfBYz54Wg2nIDU6CP8Dyybt/Wyx4D3oXmTPEaOMB62uqJvQ==",
|
||||
"requires": {
|
||||
"@react-dnd/invariant": "^2.0.0",
|
||||
"dnd-core": "^11.1.3"
|
||||
}
|
||||
},
|
||||
"react-dom": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz",
|
||||
@ -7833,6 +8002,15 @@
|
||||
"picomatch": "^2.2.1"
|
||||
}
|
||||
},
|
||||
"redux": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz",
|
||||
"integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"symbol-observable": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"regenerate": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
|
||||
@ -8256,6 +8434,11 @@
|
||||
"semver": "^6.3.0"
|
||||
}
|
||||
},
|
||||
"sax": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz",
|
||||
"integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o="
|
||||
},
|
||||
"scheduler": {
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.18.0.tgz",
|
||||
@ -9159,6 +9342,11 @@
|
||||
"xml-reader": "2.4.3"
|
||||
}
|
||||
},
|
||||
"symbol-observable": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
|
||||
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
|
||||
},
|
||||
"synchronous-promise": {
|
||||
"version": "2.0.13",
|
||||
"resolved": "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.13.tgz",
|
||||
@ -11862,6 +12050,20 @@
|
||||
"xml-lexer": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"xml2js": {
|
||||
"version": "0.4.19",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
|
||||
"integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
|
||||
"requires": {
|
||||
"sax": ">=0.6.0",
|
||||
"xmlbuilder": "~9.0.1"
|
||||
}
|
||||
},
|
||||
"xmlbuilder": {
|
||||
"version": "9.0.7",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
|
||||
"integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0="
|
||||
},
|
||||
"xregexp": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.3.0.tgz",
|
||||
|
@ -6,10 +6,11 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.10.5",
|
||||
"@reach/disclosure": "^0.10.5",
|
||||
"@reach/menu-button": "^0.10.1",
|
||||
"@reach/menu-button": "^0.10.5",
|
||||
"@reach/tabs": "^0.10.5",
|
||||
"@tlon/indigo-light": "^1.0.3",
|
||||
"@tlon/indigo-react": "^1.1.15",
|
||||
"aws-sdk": "^2.726.0",
|
||||
"classnames": "^2.2.6",
|
||||
"codemirror": "^5.55.0",
|
||||
"css-loader": "^3.5.3",
|
||||
@ -22,6 +23,9 @@
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.5.2",
|
||||
"react-codemirror2": "^6.0.1",
|
||||
"react-dnd-html5-backend": "^11.1.3",
|
||||
"react-dnd-multi-backend": "^6.0.2",
|
||||
"react-dnd-touch-backend": "^11.1.3",
|
||||
"react-dom": "^16.8.6",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-markdown": "^4.3.1",
|
||||
@ -61,6 +65,7 @@
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
"file-loader": "^6.0.0",
|
||||
"html-webpack-plugin": "^4.2.0",
|
||||
"react-dnd": "^11.1.3",
|
||||
"react-hot-loader": "^4.12.21",
|
||||
"sass": "^1.26.5",
|
||||
"sass-loader": "^8.0.2",
|
||||
|
@ -11,6 +11,7 @@ import GroupsApi from './groups';
|
||||
import LaunchApi from './launch';
|
||||
import LinksApi from './links';
|
||||
import PublishApi from './publish';
|
||||
import S3Api from './s3';
|
||||
|
||||
export default class GlobalApi extends BaseApi<StoreState> {
|
||||
chat = new ChatApi(this.ship, this.channel, this.store);
|
||||
@ -22,6 +23,7 @@ export default class GlobalApi extends BaseApi<StoreState> {
|
||||
launch = new LaunchApi(this.ship, this.channel, this.store);
|
||||
links = new LinksApi(this.ship, this.channel, this.store);
|
||||
publish = new PublishApi(this.ship, this.channel, this.store);
|
||||
s3 = new S3Api(this.ship, this.channel, this.store);
|
||||
|
||||
|
||||
constructor(public ship: Patp, public channel: any, public store: GlobalStore) {
|
||||
|
@ -12,7 +12,7 @@ export default class LaunchApi extends BaseApi<StoreState> {
|
||||
this.launchAction({ remove: name });
|
||||
}
|
||||
|
||||
changeOrder(orderedTiles = []) {
|
||||
changeOrder(orderedTiles: string[] = []) {
|
||||
this.launchAction({ 'change-order': orderedTiles });
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import BaseApi from "./base";
|
||||
import { StoreState } from "../store/type";
|
||||
import { BackgroundConfig } from "../types/local-update";
|
||||
|
||||
export default class LocalApi extends BaseApi<StoreState> {
|
||||
getBaseHash() {
|
||||
@ -38,4 +39,38 @@ export default class LocalApi extends BaseApi<StoreState> {
|
||||
});
|
||||
}
|
||||
|
||||
setBackground(backgroundConfig: BackgroundConfig) {
|
||||
this.store.handleEvent({
|
||||
data: {
|
||||
local: {
|
||||
backgroundConfig
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hideAvatars(hideAvatars: boolean) {
|
||||
this.store.handleEvent({
|
||||
data: {
|
||||
local: {
|
||||
hideAvatars
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hideNicknames(hideNicknames: boolean) {
|
||||
this.store.handleEvent({
|
||||
data: {
|
||||
local: {
|
||||
hideNicknames
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dehydrate() {
|
||||
this.store.dehydrate();
|
||||
}
|
||||
|
||||
}
|
||||
|
37
pkg/interface/src/logic/api/s3.ts
Normal file
37
pkg/interface/src/logic/api/s3.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import BaseApi from './base';
|
||||
import { StoreState } from '../store/type';
|
||||
import {S3Update} from '../../types/s3-update';
|
||||
|
||||
|
||||
export default class S3Api extends BaseApi<StoreState> {
|
||||
|
||||
setCurrentBucket(bucket: string) {
|
||||
this.s3Action({ 'set-current-bucket': bucket });
|
||||
}
|
||||
|
||||
addBucket(bucket: string) {
|
||||
this.s3Action({ 'add-bucket': bucket });
|
||||
}
|
||||
|
||||
removeBucket(bucket: string) {
|
||||
this.s3Action({ 'remove-bucket': bucket });
|
||||
}
|
||||
|
||||
setEndpoint(endpoint: string) {
|
||||
this.s3Action({ 'set-endpoint': endpoint });
|
||||
}
|
||||
|
||||
setAccessKeyId(accessKeyId: string) {
|
||||
this.s3Action({ 'set-access-key-id': accessKeyId });
|
||||
}
|
||||
|
||||
setSecretAccessKey(secretAccessKey: string) {
|
||||
this.s3Action({ 'set-secret-access-key': secretAccessKey });
|
||||
}
|
||||
|
||||
private s3Action(data: any) {
|
||||
this.action('s3-store', 's3-action', data);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -40,6 +40,9 @@ const commandIndex = function () {
|
||||
commands.push(obj);
|
||||
}
|
||||
});
|
||||
|
||||
commands.push(result('Profile', '/~profile', 'profile', null));
|
||||
|
||||
return commands;
|
||||
};
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
import S3 from 'aws-sdk/clients/s3';
|
||||
|
||||
export default class S3Client {
|
||||
constructor() {
|
||||
this.s3 = null;
|
||||
@ -8,27 +10,20 @@ export default class S3Client {
|
||||
}
|
||||
|
||||
setCredentials(endpoint, accessKeyId, secretAccessKey) {
|
||||
if (!window.AWS) {
|
||||
setTimeout(() => {
|
||||
this.setCredentials(endpoint, accessKeyId, secretAccessKey);
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
this.endpoint = new window.AWS.Endpoint(endpoint);
|
||||
this.endpoint = endpoint;
|
||||
this.accessKeyId = accessKeyId;
|
||||
this.secretAccessKey = secretAccessKey;
|
||||
|
||||
this.s3 =
|
||||
new window.AWS.S3({
|
||||
endpoint: this.endpoint,
|
||||
credentials: new window.AWS.Credentials({
|
||||
accessKeyId: this.accessKeyId,
|
||||
secretAccessKey: this.secretAccessKey
|
||||
})
|
||||
});
|
||||
this.s3 = new S3({
|
||||
endpoint: endpoint,
|
||||
credentials: {
|
||||
accessKeyId: this.accessKeyId,
|
||||
secretAccessKey: this.secretAccessKey
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
upload(bucket, filename, buffer) {
|
||||
async upload(bucket, filename, buffer) {
|
||||
const params = {
|
||||
Bucket: bucket,
|
||||
Key: filename,
|
||||
@ -36,19 +31,11 @@ export default class S3Client {
|
||||
ACL: 'public-read',
|
||||
ContentType: buffer.type
|
||||
};
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.s3) {
|
||||
reject({ error: 'S3 not initialized!' });
|
||||
return;
|
||||
}
|
||||
this.s3.upload(params, (error, data) => {
|
||||
if (error) {
|
||||
reject({ error });
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if(!this.s3) {
|
||||
throw new Error('S3 not initialized');
|
||||
}
|
||||
return this.s3.upload(params).promise();
|
||||
}
|
||||
}
|
||||
|
||||
|
51
pkg/interface/src/logic/lib/useS3.ts
Normal file
51
pkg/interface/src/logic/lib/useS3.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { useCallback, useMemo, useEffect, useRef } from "react";
|
||||
import { S3State } from "../../types/s3-update";
|
||||
import S3 from "aws-sdk/clients/s3";
|
||||
|
||||
export function useS3(s3: S3State) {
|
||||
const { configuration, credentials } = s3;
|
||||
|
||||
const client = useRef<S3 | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!credentials) {
|
||||
return;
|
||||
}
|
||||
client.current = new S3({ credentials, endpoint: credentials.endpoint });
|
||||
}, [credentials]);
|
||||
|
||||
const canUpload = useMemo(
|
||||
() =>
|
||||
(client && credentials && configuration.currentBucket !== "") || false,
|
||||
[credentials, configuration.currentBucket, client]
|
||||
);
|
||||
|
||||
const uploadDefault = useCallback(async (file: File) => {
|
||||
if (configuration.currentBucket === "") {
|
||||
throw new Error("current bucket not set");
|
||||
}
|
||||
return upload(file, configuration.currentBucket);
|
||||
}, []);
|
||||
|
||||
const upload = useCallback(
|
||||
async (file: File, bucket: string) => {
|
||||
if (!client.current) {
|
||||
throw new Error("S3 not ready");
|
||||
}
|
||||
|
||||
const params = {
|
||||
Bucket: bucket,
|
||||
Key: file.name,
|
||||
Body: file,
|
||||
ACL: "public-read",
|
||||
ContentType: file.type,
|
||||
};
|
||||
|
||||
const { Location } = await client.current.upload(params).promise();
|
||||
return Location;
|
||||
},
|
||||
[client]
|
||||
);
|
||||
|
||||
return { canUpload, upload, uploadDefault };
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
export const MOBILE_BROWSER_REGEX = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i;
|
||||
|
||||
export function resourceAsPath(resource) {
|
||||
const { name, ship } = resource;
|
||||
return `/ship/~${ship}/${name}`;
|
||||
@ -75,6 +77,14 @@ export function uxToHex(ux) {
|
||||
return value;
|
||||
}
|
||||
|
||||
export function hexToUx(hex) {
|
||||
const ux = _.chain(hex.split(""))
|
||||
.chunk(4)
|
||||
.map((x) => _.dropWhile(x, (y) => y === 0).join(""))
|
||||
.join(".");
|
||||
return `0x${ux}`;
|
||||
}
|
||||
|
||||
function hexToDec(hex) {
|
||||
const alphabet = '0123456789ABCDEF'.split('');
|
||||
return hex.reverse().reduce((acc, digit, idx) => {
|
||||
|
@ -1,17 +1,35 @@
|
||||
import _ from 'lodash';
|
||||
import { StoreState } from '../../store/type';
|
||||
import { StoreState } from '~/store/type';
|
||||
import { Cage } from '~/types/cage';
|
||||
import { LocalUpdate } from '~/types/local-update';
|
||||
import { LocalUpdate, BackgroundConfig } from '~/types/local-update';
|
||||
|
||||
type LocalState = Pick<StoreState, 'sidebarShown' | 'omniboxShown' | 'dark' | 'baseHash' | 'suspendedFocus'>;
|
||||
type LocalState = Pick<StoreState, 'sidebarShown' | 'omniboxShown' | 'baseHash' | 'hideAvatars' | 'hideNicknames' | 'background' | 'dark' | 'suspendedFocus'>;
|
||||
|
||||
export default class LocalReducer<S extends LocalState> {
|
||||
rehydrate(state: S) {
|
||||
try {
|
||||
const json = JSON.parse(localStorage.getItem('localReducer') || '');
|
||||
_.forIn(json, (value, key) => {
|
||||
state[key] = value;
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('Failed to rehydrate localStorage state', e);
|
||||
}
|
||||
}
|
||||
|
||||
dehydrate(state: S) {
|
||||
const json = _.pick(state, ['hideNicknames' , 'hideAvatars' , 'background']);
|
||||
localStorage.setItem('localReducer', JSON.stringify(json));
|
||||
}
|
||||
reduce(json: Cage, state: S) {
|
||||
const data = json['local'];
|
||||
if (data) {
|
||||
this.sidebarToggle(data, state);
|
||||
this.setDark(data, state);
|
||||
this.baseHash(data, state);
|
||||
this.backgroundConfig(data, state)
|
||||
this.hideAvatars(data, state)
|
||||
this.hideNicknames(data, state)
|
||||
this.omniboxShown(data, state);
|
||||
}
|
||||
}
|
||||
@ -45,4 +63,22 @@ export default class LocalReducer<S extends LocalState> {
|
||||
state.dark = obj.setDark;
|
||||
}
|
||||
}
|
||||
|
||||
backgroundConfig(obj: LocalUpdate, state: S) {
|
||||
if('backgroundConfig' in obj) {
|
||||
state.background = obj.backgroundConfig;
|
||||
}
|
||||
}
|
||||
|
||||
hideAvatars(obj: LocalUpdate, state: S) {
|
||||
if('hideAvatars' in obj) {
|
||||
state.hideAvatars = obj.hideAvatars;
|
||||
}
|
||||
}
|
||||
|
||||
hideNicknames(obj: LocalUpdate, state: S) {
|
||||
if( 'hideNicknames' in obj) {
|
||||
state.hideNicknames = obj.hideNicknames;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ export default class S3Reducer<S extends S3State> {
|
||||
currentBucket(json: S3Update, state: S) {
|
||||
const data = _.get(json, 'setCurrentBucket', false);
|
||||
if (data && state.s3) {
|
||||
|
||||
state.s3.configuration.currentBucket = data;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,10 @@ export default class BaseStore<S extends object> {
|
||||
this.state = this.initialState();
|
||||
}
|
||||
|
||||
dehydrate() {}
|
||||
|
||||
rehydrate() {}
|
||||
|
||||
initialState() {
|
||||
return {} as S;
|
||||
}
|
||||
|
@ -34,6 +34,14 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
||||
launchReducer = new LaunchReducer();
|
||||
connReducer = new ConnectionReducer();
|
||||
|
||||
rehydrate() {
|
||||
this.localReducer.rehydrate(this.state);
|
||||
}
|
||||
|
||||
dehydrate() {
|
||||
this.localReducer.dehydrate(this.state);
|
||||
}
|
||||
|
||||
|
||||
initialState(): StoreState {
|
||||
return {
|
||||
@ -44,6 +52,9 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
||||
omniboxShown: false,
|
||||
suspendedFocus: null,
|
||||
baseHash: null,
|
||||
background: undefined,
|
||||
hideAvatars: false,
|
||||
hideNicknames: false,
|
||||
invites: {},
|
||||
associations: {
|
||||
chat: {},
|
||||
|
@ -11,6 +11,7 @@ import { Permissions } from '~/types/permission-update';
|
||||
import { LaunchState, WeatherState } from '~/types/launch-update';
|
||||
import { LinkComments, LinkCollections, LinkSeen } from '~/types/link-update';
|
||||
import { ConnectionStatus } from '~/types/connection';
|
||||
import { BackgroundConfig } from '~/types/local-update';
|
||||
|
||||
export interface StoreState {
|
||||
// local state
|
||||
@ -20,6 +21,9 @@ export interface StoreState {
|
||||
dark: boolean;
|
||||
connection: ConnectionStatus;
|
||||
baseHash: string | null;
|
||||
background: BackgroundConfig;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
// invite state
|
||||
invites: Invites;
|
||||
// metadata state
|
||||
|
18
pkg/interface/src/types/index.ts
Normal file
18
pkg/interface/src/types/index.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export * from './cage';
|
||||
export * from './chat-hook-update';
|
||||
export * from './chat-update';
|
||||
export * from './connection';
|
||||
export * from './contact-update';
|
||||
export * from './global';
|
||||
export * from './group-update';
|
||||
export * from './invite-update';
|
||||
export * from './launch-update';
|
||||
export * from './link-listen-update';
|
||||
export * from './link-update';
|
||||
export * from './local-update';
|
||||
export * from './metadata-update';
|
||||
export * from './noun';
|
||||
export * from './permission-update';
|
||||
export * from './publish-response';
|
||||
export * from './publish-update';
|
||||
export * from './s3-update';
|
@ -33,14 +33,14 @@ export interface LaunchState {
|
||||
}
|
||||
}
|
||||
|
||||
interface Tile {
|
||||
export interface Tile {
|
||||
isShown: boolean;
|
||||
type: TileType;
|
||||
}
|
||||
|
||||
type TileType = TileTypeBasic | TileTypeCustom;
|
||||
|
||||
interface TileTypeBasic {
|
||||
export interface TileTypeBasic {
|
||||
basic: {
|
||||
iconUrl: string;
|
||||
linkedUrl: string;
|
||||
|
@ -1,8 +1,11 @@
|
||||
export type LocalUpdate =
|
||||
LocalUpdateSidebarToggle
|
||||
| LocalUpdateSetDark
|
||||
| LocalUpdateSetOmniboxShown
|
||||
| LocalUpdateBaseHash;
|
||||
| LocalUpdateBaseHash
|
||||
| LocalUpdateBackgroundConfig
|
||||
| LocalUpdateHideAvatars
|
||||
| LocalUpdateHideNicknames
|
||||
| LocalUpdateSetOmniboxShown;
|
||||
|
||||
interface LocalUpdateSidebarToggle {
|
||||
sidebarToggle: boolean;
|
||||
@ -16,6 +19,30 @@ interface LocalUpdateBaseHash {
|
||||
baseHash: string;
|
||||
}
|
||||
|
||||
interface LocalUpdateBackgroundConfig {
|
||||
backgroundConfig: BackgroundConfig;
|
||||
}
|
||||
|
||||
interface LocalUpdateHideAvatars {
|
||||
hideAvatars: boolean;
|
||||
}
|
||||
|
||||
interface LocalUpdateHideNicknames {
|
||||
hideNicknames: boolean;
|
||||
}
|
||||
|
||||
export type BackgroundConfig = BackgroundConfigUrl | BackgroundConfigColor | undefined;
|
||||
|
||||
interface BackgroundConfigUrl {
|
||||
type: 'url';
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface BackgroundConfigColor {
|
||||
type: 'color';
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface LocalUpdateSetOmniboxShown {
|
||||
omniboxShown: boolean;
|
||||
}
|
||||
|
@ -31,6 +31,13 @@ const Root = styled.div`
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
${p => p.background?.type === 'url' ? `
|
||||
background-image: url('${p.background?.url}');
|
||||
background-size: cover;
|
||||
` : p.background?.type === 'color' ? `
|
||||
background-color: ${p.background.color};
|
||||
` : ``
|
||||
}
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
`;
|
||||
@ -60,6 +67,7 @@ class App extends React.Component {
|
||||
this.api.local.setDark(this.themeWatcher.matches);
|
||||
this.themeWatcher.addListener(this.updateTheme);
|
||||
this.api.local.getBaseHash();
|
||||
this.store.rehydrate();
|
||||
Mousetrap.bindGlobal(['command+/', 'ctrl+/'], (e) => {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
@ -96,15 +104,16 @@ class App extends React.Component {
|
||||
const associations = state.associations ?
|
||||
state.associations : { contacts: {} };
|
||||
const theme = state.dark ? dark : light;
|
||||
const { background } = state;
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Helmet>
|
||||
{window.ship.length < 14
|
||||
? <link rel="icon" type="image/svg+xml" href={this.faviconString()} />
|
||||
? <link rel="icon" type="image/svg+xml" href={this.faviconString()} />
|
||||
: null}
|
||||
</Helmet>
|
||||
<Root>
|
||||
<Root background={background} >
|
||||
<Router>
|
||||
<StatusBarWithRouter
|
||||
props={this.props}
|
||||
@ -113,6 +122,7 @@ class App extends React.Component {
|
||||
api={this.api}
|
||||
connection={this.state.connection}
|
||||
subscription={this.subscription}
|
||||
ship={this.ship}
|
||||
/>
|
||||
<Omnibox
|
||||
associations={state.associations}
|
||||
|
@ -87,7 +87,9 @@ export default class ChatApp extends React.Component<ChatAppProps, {}> {
|
||||
api,
|
||||
chatInitialized,
|
||||
pendingMessages,
|
||||
groups
|
||||
groups,
|
||||
hideAvatars,
|
||||
hideNicknames
|
||||
} = props;
|
||||
|
||||
const renderChannelSidebar = (props, station?) => (
|
||||
@ -267,6 +269,8 @@ export default class ChatApp extends React.Component<ChatAppProps, {}> {
|
||||
popout={popout}
|
||||
sidebarShown={sidebarShown}
|
||||
chatInitialized={chatInitialized}
|
||||
hideAvatars={hideAvatars}
|
||||
hideNicknames={hideNicknames}
|
||||
{...props}
|
||||
/>
|
||||
</Skeleton>
|
||||
|
@ -36,6 +36,8 @@ type ChatScreenProps = RouteComponentProps<{
|
||||
sidebarShown: boolean;
|
||||
chatInitialized: boolean;
|
||||
envelopes: Envelope[];
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
};
|
||||
|
||||
interface ChatScreenState {
|
||||
@ -126,7 +128,10 @@ export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
|
||||
group={props.group}
|
||||
ship={props.match.params.ship}
|
||||
station={props.station}
|
||||
api={props.api} />
|
||||
api={props.api}
|
||||
hideNicknames={props.hideNicknames}
|
||||
hideAvatars={props.hideAvatars}
|
||||
/>
|
||||
<ChatInput
|
||||
api={props.api}
|
||||
numMsgs={lastMsgNum}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import { UnControlled as CodeEditor } from 'react-codemirror2';
|
||||
import { MOBILE_BROWSER_REGEX } from "~/logic/lib/util";
|
||||
import CodeMirror from 'codemirror';
|
||||
|
||||
import 'codemirror/mode/markdown/markdown';
|
||||
@ -132,7 +133,7 @@ export default class ChatEditor extends Component {
|
||||
onChange={(e, d, v) => this.messageChange(e, d, v)}
|
||||
editorDidMount={(editor) => {
|
||||
this.editor = editor;
|
||||
if (!BROWSER_REGEX.test(navigator.userAgent)) {
|
||||
if (!MOBILE_BROWSER_REGEX.test(navigator.userAgent)) {
|
||||
editor.focus();
|
||||
}
|
||||
}}
|
||||
|
@ -15,7 +15,9 @@ export const ChatMessage = (props) => {
|
||||
group,
|
||||
association,
|
||||
contacts,
|
||||
unreadRef
|
||||
unreadRef,
|
||||
hideAvatars,
|
||||
hideNicknames
|
||||
} = props;
|
||||
|
||||
// Render sigil if previous message is not by the same sender
|
||||
@ -42,6 +44,8 @@ export const ChatMessage = (props) => {
|
||||
group={group}
|
||||
contacts={contacts}
|
||||
association={association}
|
||||
hideNicknames={hideNicknames}
|
||||
hideAvatars={hideAvatars}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -184,7 +184,10 @@ export class ChatWindow extends Component {
|
||||
nextMsg={messages[i + 1]}
|
||||
association={props.association}
|
||||
group={props.group}
|
||||
contacts={props.contacts} />
|
||||
contacts={props.contacts}
|
||||
hideAvatars={props.hideAvatars}
|
||||
hideNicknames={props.hideNicknames}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</ChatScrollContainer>
|
||||
|
@ -17,6 +17,7 @@ export const Message = (props) => {
|
||||
props.renderSigil ? 'hh:mm a' : 'hh:mm'
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<div className={containerClass}
|
||||
style={{
|
||||
@ -46,12 +47,14 @@ const renderWithSigil = (props, timestamp) => {
|
||||
|
||||
const contact = props.msg.author in props.contacts
|
||||
? props.contacts[props.msg.author] : false;
|
||||
const showNickname = !props.hideNicknames && contact?.nickname;
|
||||
let name = `~${props.msg.author}`;
|
||||
let color = '#000000';
|
||||
let sigilClass = 'mix-blend-diff';
|
||||
if (contact) {
|
||||
name = (contact.nickname.length > 0)
|
||||
? contact.nickname : `~${props.msg.author}`;
|
||||
name = showNickname
|
||||
? contact.nickname
|
||||
: `~${props.msg.author}`;
|
||||
color = `#${uxToHex(contact.color)}`;
|
||||
sigilClass = '';
|
||||
}
|
||||
@ -69,6 +72,8 @@ const renderWithSigil = (props, timestamp) => {
|
||||
sigilClass={sigilClass}
|
||||
association={props.association}
|
||||
group={props.group}
|
||||
hideAvatars={props.hideAvatars}
|
||||
hideNicknames={props.hideNicknames}
|
||||
className="fl pr3 v-top bg-white bg-gray0-d"
|
||||
/>
|
||||
<div className="fr clamp-message white-d"
|
||||
@ -78,7 +83,7 @@ const renderWithSigil = (props, timestamp) => {
|
||||
<span
|
||||
className={
|
||||
'mw5 db truncate pointer ' +
|
||||
(contact.nickname ? '' : 'mono')
|
||||
(showNickname ? '' : 'mono')
|
||||
}
|
||||
onClick={() => {
|
||||
writeText(props.msg.author);
|
||||
|
@ -62,8 +62,9 @@ export class OverlaySigil extends Component {
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
const { hideAvatars } = props;
|
||||
|
||||
const img = (props.contact && (props.contact.avatar !== null))
|
||||
const img = (props.contact && (props.contact.avatar !== null) && !hideAvatars)
|
||||
? <img src={props.contact.avatar} height={24} width={24} className="dib" />
|
||||
: <Sigil
|
||||
ship={props.ship}
|
||||
@ -89,6 +90,8 @@ export class OverlaySigil extends Component {
|
||||
association={props.association}
|
||||
group={props.group}
|
||||
onDismiss={this.profileHide}
|
||||
hideAvatars={hideAvatars}
|
||||
hideNicknames={props.hideNicknames}
|
||||
/>
|
||||
)}
|
||||
{img}
|
||||
|
@ -34,7 +34,7 @@ export class ProfileOverlay extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { contact, ship, color, topSpace, bottomSpace, group, association } = this.props;
|
||||
const { contact, ship, color, topSpace, bottomSpace, group, association, hideNicknames, hideAvatars } = this.props;
|
||||
|
||||
let top, bottom;
|
||||
if (topSpace < OVERLAY_HEIGHT / 2) {
|
||||
@ -51,10 +51,10 @@ export class ProfileOverlay extends Component {
|
||||
const isOwn = window.ship === ship;
|
||||
|
||||
const identityHref = group.hidden
|
||||
? '/~groups/me'
|
||||
? '/~profile/identity'
|
||||
: `/~groups/view${association['group-path']}/${window.ship}`;
|
||||
|
||||
let img = (contact && (contact.avatar !== null))
|
||||
let img = contact?.avatar && !hideAvatars
|
||||
? <img src={contact.avatar} height={160} width={160} className="brt2 dib" />
|
||||
: <Sigil
|
||||
ship={ship}
|
||||
@ -63,6 +63,7 @@ export class ProfileOverlay extends Component {
|
||||
classes="brt2"
|
||||
svgClass="brt2"
|
||||
/>;
|
||||
const showNickname = contact?.nickname && !hideNicknames;
|
||||
|
||||
if (!group.hidden) {
|
||||
img = <Link to={`/~groups/view${association['group-path']}/${ship}`}>{img}</Link>;
|
||||
@ -78,7 +79,7 @@ export class ProfileOverlay extends Component {
|
||||
{img}
|
||||
</div>
|
||||
<div className="pv3 pl3 pr2">
|
||||
{contact && contact.nickname && (
|
||||
{showNickname && (
|
||||
<div className="b white-d truncate">{contact.nickname}</div>
|
||||
)}
|
||||
<div className="mono gray2">{cite(`~${ship}`)}</div>
|
||||
|
@ -31,9 +31,9 @@ export class Skeleton extends Component {
|
||||
|
||||
return (
|
||||
// app outer skeleton
|
||||
<div className={'h-100 w-100 bg-gray0-d ' + popoutWindow}>
|
||||
<div className={'h-100 w-100 ' + popoutWindow}>
|
||||
{/* app window borders */}
|
||||
<div className={ 'cf w-100 flex h-100 ' + popoutBorder }>
|
||||
<div className={ 'bg-white bg-gray0-d cf w-100 flex h-100 ' + popoutBorder }>
|
||||
{/* sidebar skeleton, hidden on mobile when in chat panel */}
|
||||
<div
|
||||
className={
|
||||
|
@ -333,33 +333,6 @@ export default class GroupsApp extends Component<GroupsAppProps, {}> {
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route exact path="/~groups/me"
|
||||
render={(props) => {
|
||||
const me = defaultContacts[window.ship] || {};
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
history={props.history}
|
||||
api={api}
|
||||
contacts={contacts}
|
||||
groups={groups}
|
||||
invites={invites}
|
||||
activeDrawer="rightPanel"
|
||||
selected="me"
|
||||
associations={associations}
|
||||
>
|
||||
<ContactCard
|
||||
api={api}
|
||||
history={props.history}
|
||||
path="/~/default"
|
||||
contact={me}
|
||||
s3={s3}
|
||||
ship={window.ship}
|
||||
/>
|
||||
</Skeleton>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
</>
|
||||
);
|
||||
|
@ -0,0 +1,124 @@
|
||||
import React, { Component } from "react";
|
||||
import { Sigil } from "~/logic/lib/sigil";
|
||||
import * as Yup from "yup";
|
||||
|
||||
import { Link } from "react-router-dom";
|
||||
import { EditElement } from "./edit-element";
|
||||
import { Spinner } from "~/views/components/Spinner";
|
||||
import { uxToHex } from "~/logic/lib/util";
|
||||
import { Col, Input, Box, Text, Row } from "@tlon/indigo-react";
|
||||
import { Formik, Form, FormikHelpers } from "formik";
|
||||
import { Contact } from "~/types/contact-update";
|
||||
import { AsyncButton } from "~/views/components/AsyncButton";
|
||||
import { ColorInput } from "~/views/components/ColorInput";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { ImageInput } from "~/views/components/ImageInput";
|
||||
import { S3State } from "~/types";
|
||||
|
||||
interface ContactCardProps {
|
||||
contact: Contact;
|
||||
path: string;
|
||||
api: GlobalApi;
|
||||
s3: S3State;
|
||||
}
|
||||
|
||||
const formSchema = Yup.object({
|
||||
color: Yup.string(),
|
||||
nickname: Yup.string(),
|
||||
email: Yup.string().matches(
|
||||
new RegExp(
|
||||
String(
|
||||
/[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*/.source
|
||||
) +
|
||||
/@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/
|
||||
.source
|
||||
),
|
||||
"Not a valid email"
|
||||
),
|
||||
phone: Yup.string().matches(
|
||||
new RegExp(
|
||||
String(/^\s*(?:\+?(\d{1,3}))?/.source) +
|
||||
/([-. (]*(\d{3})[-. )]*)?((\d{3})[-. ]*(\d{2,4})(?:[-.x ]*(\d+))?)\s*$/
|
||||
.source
|
||||
),
|
||||
"Not a valid phone"
|
||||
),
|
||||
|
||||
website: Yup.string().matches(
|
||||
new RegExp(
|
||||
String(/[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}/.source) +
|
||||
/\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/.source
|
||||
),
|
||||
"Not a valid website"
|
||||
),
|
||||
});
|
||||
|
||||
export function ContactCard(props: ContactCardProps) {
|
||||
const us = `~${window.ship}`;
|
||||
const { contact } = props;
|
||||
const onSubmit = async (values: Contact, actions: FormikHelpers<Contact>) => {
|
||||
try {
|
||||
await Object.keys(values).reduce((acc, key) => {
|
||||
const newValue = key !== "color" ? values[key] : uxToHex(values[key]);
|
||||
if (newValue !== contact[key]) {
|
||||
if (key === "avatar") {
|
||||
return acc.then(() =>
|
||||
props.api.contacts.edit(props.path, us, {
|
||||
avatar: { url: newValue },
|
||||
} as any)
|
||||
);
|
||||
}
|
||||
|
||||
return acc.then(() =>
|
||||
props.api.contacts.edit(props.path, us, {
|
||||
[key]: newValue,
|
||||
} as any)
|
||||
);
|
||||
}
|
||||
return acc;
|
||||
}, Promise.resolve());
|
||||
actions.setStatus({ success: null });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
actions.setStatus({ error: e.message });
|
||||
}
|
||||
};
|
||||
|
||||
const hexColor = contact.color ? `#${uxToHex(contact.color)}` : "#000000";
|
||||
|
||||
return (
|
||||
<Box p={4} height="100%" overflowY="auto">
|
||||
<Formik
|
||||
validationSchema={formSchema}
|
||||
initialValues={contact}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Form>
|
||||
<Col>
|
||||
<Row
|
||||
borderBottom={1}
|
||||
borderBottomColor="washedGray"
|
||||
pb={3}
|
||||
alignItems="center"
|
||||
>
|
||||
<Sigil size={32} classes="" color={hexColor} ship={us} />
|
||||
<Box ml={2}>
|
||||
<Text fontFamily="mono">{us}</Text>
|
||||
</Box>
|
||||
</Row>
|
||||
<ImageInput mt={3} id="avatar" label="Avatar" s3={props.s3} />
|
||||
<ColorInput id="color" label="Sigil Color" />
|
||||
<Input id="nickname" label="Nickname" />
|
||||
<Input id="email" label="Email" />
|
||||
<Input id="phone" label="Phone" />
|
||||
<Input id="website" label="Website" />
|
||||
<Input id="notes" label="Notes" />
|
||||
<AsyncButton primary loadingText="Updating..." border>
|
||||
Save
|
||||
</AsyncButton>
|
||||
</Col>
|
||||
</Form>
|
||||
</Formik>
|
||||
</Box>
|
||||
);
|
||||
}
|
@ -18,30 +18,6 @@ export class GroupSidebar extends Component {
|
||||
|
||||
const selectedClass = (props.selected === 'me') ? 'bg-gray4 bg-gray1-d' : 'bg-white bg-gray0-d';
|
||||
|
||||
const rootIdentity = <Link
|
||||
key={1}
|
||||
to={'/~groups/me'}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
'w-100 pl4 pt1 pb1 f9 flex justify-start content-center ' +
|
||||
selectedClass}
|
||||
>
|
||||
<Sigil
|
||||
ship={window.ship}
|
||||
color="#000000"
|
||||
classes="mix-blend-diff"
|
||||
size={32}
|
||||
/>
|
||||
<p
|
||||
className="f9 w-70 dib v-mid ml2 nowrap mono"
|
||||
style={{ paddingTop: 6 }}
|
||||
>
|
||||
{cite(window.ship)}
|
||||
</p>
|
||||
</div>
|
||||
</Link>;
|
||||
|
||||
const inviteItems =
|
||||
Object.keys(props.invites)
|
||||
.map((uid) => {
|
||||
@ -127,8 +103,6 @@ export class GroupSidebar extends Component {
|
||||
<p className="f9 pt4 pl4 green2 bn">Join Group</p>
|
||||
</Link>
|
||||
<Welcome contacts={props.contacts} />
|
||||
<h2 className="f9 pt4 pr4 pb2 pl4 gray2 c-default">Your Identity</h2>
|
||||
{rootIdentity}
|
||||
{inviteItems}
|
||||
<h2 className="f9 pt4 pr4 pb2 pl4 gray2 c-default">Groups</h2>
|
||||
{groupItems}
|
||||
|
@ -10,7 +10,7 @@ export class Skeleton extends Component {
|
||||
|
||||
return (
|
||||
<div className="h-100 w-100 ph4-m ph4-l ph4-xl pb4-m pb4-l pb4-xl">
|
||||
<div className="cf w-100 h-100 flex ba-m ba-l ba-xl b--gray4 b--gray1-d br1">
|
||||
<div className="bg-white bg-gray0-d cf w-100 h-100 flex ba-m ba-l ba-xl b--gray4 b--gray1-d br1">
|
||||
<GroupSidebar
|
||||
contacts={props.contacts}
|
||||
groups={props.groups}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import React from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
|
||||
import { Box } from '@tlon/indigo-react';
|
||||
|
||||
import './css/custom.css';
|
||||
|
||||
import Tiles from './components/tiles';
|
||||
@ -33,7 +35,20 @@ export default class LaunchApp extends React.Component {
|
||||
weather={props.weather}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute mono bottom-0 left-0 f9 gray2 ml4 mb4 f8"> {props.baseHash} </div>
|
||||
<Box
|
||||
position="absolute"
|
||||
fontFamily="mono"
|
||||
left="0"
|
||||
bottom="0"
|
||||
color="gray"
|
||||
bg="white"
|
||||
ml={3}
|
||||
mb={3}
|
||||
borderRadius={2}
|
||||
fontSize={0}
|
||||
p={2}>
|
||||
{props.baseHash}
|
||||
</Box>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -192,7 +192,7 @@ class Clock extends React.Component {
|
||||
const newX = cx + (ctr - 15) * Math.cos(this.angle);
|
||||
const newY = cy + (ctr - 15) * Math.sin(this.angle);
|
||||
|
||||
// Initial background
|
||||
// Center white circle with time and date
|
||||
circle(
|
||||
ctx,
|
||||
ctr,
|
||||
@ -333,18 +333,6 @@ class Clock extends React.Component {
|
||||
);
|
||||
|
||||
// Outer borders
|
||||
circleOutline(
|
||||
ctx,
|
||||
ctr,
|
||||
ctr,
|
||||
ctr,
|
||||
-1,
|
||||
2 * Math.PI,
|
||||
background,
|
||||
1
|
||||
);
|
||||
|
||||
// Center white circle with time and date
|
||||
circle(
|
||||
ctx,
|
||||
ctr,
|
||||
@ -352,7 +340,8 @@ class Clock extends React.Component {
|
||||
ctr/1.85,
|
||||
-1,
|
||||
2 * Math.PI,
|
||||
background
|
||||
background,
|
||||
1
|
||||
);
|
||||
|
||||
// Center white circle border
|
||||
@ -401,7 +390,7 @@ export default class ClockTile extends React.Component {
|
||||
|
||||
renderWrapper(child) {
|
||||
return (
|
||||
<Tile>
|
||||
<Tile transparent>
|
||||
{child}
|
||||
</Tile>
|
||||
);
|
||||
|
@ -3,10 +3,12 @@ import React from 'react';
|
||||
|
||||
export default class Tile extends React.Component {
|
||||
render() {
|
||||
const { transparent } = this.props;
|
||||
const bgClasses = transparent ? ' ' : ' bg-white bg-gray0-d ';
|
||||
return (
|
||||
<div className="fl ma2 bg-white bg-gray0-d overflow-hidden"
|
||||
style={{ height: '126px', width: '126px' }}>
|
||||
{this.props.children}
|
||||
<div className={"fl ma2 overflow-hidden" + bgClasses}
|
||||
style={{ height: '126px', width: '126px' }}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ export class LinksApp extends Component {
|
||||
|
||||
const listening = props.linkListening;
|
||||
|
||||
const { api, sidebarShown } = this.props;
|
||||
const { api, sidebarShown, hideAvatars, hideNicknames } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -261,7 +261,9 @@ export class LinksApp extends Component {
|
||||
popout={popout}
|
||||
sidebarShown={sidebarShown}
|
||||
api={api}
|
||||
/>
|
||||
hideNicknames={hideNicknames}
|
||||
hideAvatars={hideAvatars}
|
||||
/>
|
||||
</Skeleton>
|
||||
);
|
||||
}}
|
||||
@ -321,6 +323,8 @@ export class LinksApp extends Component {
|
||||
comments={coms}
|
||||
commentPage={commentPage}
|
||||
api={api}
|
||||
hideAvatars={hideAvatars}
|
||||
hideNicknames={hideNicknames}
|
||||
/>
|
||||
</Skeleton>
|
||||
);
|
||||
|
@ -37,7 +37,9 @@ export class CommentItem extends Component {
|
||||
|
||||
const pending = props.pending ? 'o-60' : '';
|
||||
|
||||
const img = (props.avatar)
|
||||
const showAvatar = props.avatar && !props.hideAvatars
|
||||
const showNickname = props.nickname && !props.hideNicknames;
|
||||
const img = showAvatar
|
||||
? <img src={props.avatar} height={36} width={36} className="dib" />
|
||||
: <Sigil
|
||||
ship={'~' + props.ship}
|
||||
@ -54,7 +56,7 @@ export class CommentItem extends Component {
|
||||
<span className={'black white-d ' + props.nameClass}
|
||||
title={props.ship}
|
||||
>
|
||||
{props.nickname ? props.nickname : cite(props.ship)}
|
||||
{showNickname ? props.nickname : cite(props.ship)}
|
||||
</span>
|
||||
<span className="ml2">
|
||||
{this.state.timeSinceComment}
|
||||
|
@ -40,6 +40,8 @@ export class Comments extends Component {
|
||||
? props.comments.totalPages
|
||||
: 1;
|
||||
|
||||
const { hideNicknames, hideAvatars } = props;
|
||||
|
||||
const commentsList = Object.keys(commentsPage)
|
||||
.map((entry) => {
|
||||
const commentObj = commentsPage[entry];
|
||||
@ -51,7 +53,7 @@ export class Comments extends Component {
|
||||
|
||||
const { nickname, color, member, avatar } = getContactDetails(contacts[ship]);
|
||||
|
||||
const nameClass = nickname ? 'inter' : 'mono';
|
||||
const nameClass = nickname && hideNicknames ? 'inter' : 'mono';
|
||||
|
||||
return(
|
||||
<CommentItem
|
||||
@ -64,6 +66,8 @@ export class Comments extends Component {
|
||||
color={color}
|
||||
avatar={avatar}
|
||||
member={member}
|
||||
hideNicknames={hideNicknames}
|
||||
hideAvatars={hideAvatars}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -112,7 +112,9 @@ export class LinkPreview extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
const nameClass = props.nickname ? 'inter' : 'mono';
|
||||
const showNickname = props.nickname && !props.hideNicknames;
|
||||
|
||||
const nameClass = showNickname ? 'inter' : 'mono';
|
||||
|
||||
return (
|
||||
<div className="pb6 w-100">
|
||||
@ -132,7 +134,7 @@ export class LinkPreview extends Component {
|
||||
<span className={'f9 pr2 white-d dib ' + nameClass}
|
||||
title={props.ship}
|
||||
>
|
||||
{props.nickname ? props.nickname : cite(props.ship)}
|
||||
{showNickname ? props.nickname : cite(props.ship)}
|
||||
</span>
|
||||
<span className="f9 inter gray2 pr3 dib">
|
||||
{this.state.timeSinceLinkPost}
|
||||
|
@ -40,8 +40,6 @@ export class LinkItem extends Component {
|
||||
render() {
|
||||
const props = this.props;
|
||||
|
||||
const mono = (props.nickname) ? 'inter white-d' : 'mono white-d';
|
||||
|
||||
const URLparser = new RegExp(/((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/);
|
||||
|
||||
let hostname = URLparser.exec(props.url);
|
||||
@ -58,7 +56,12 @@ export class LinkItem extends Component {
|
||||
|
||||
const member = this.props.member || false;
|
||||
|
||||
const img = (this.props.avatar)
|
||||
const showAvatar = props.avatar && !props.hideAvatars;
|
||||
const showNickname = props.nickname && !props.hideNicknames;
|
||||
|
||||
const mono = showNickname ? 'inter white-d' : 'mono white-d';
|
||||
|
||||
const img = showAvatar
|
||||
? <img src={this.props.avatar} height={38} width={38} className="dib" />
|
||||
: <Sigil
|
||||
ship={'~' + props.ship}
|
||||
@ -84,7 +87,7 @@ export class LinkItem extends Component {
|
||||
<span className={'f9 pr2 dib ' + mono}
|
||||
title={props.ship}
|
||||
>
|
||||
{(props.nickname)
|
||||
{showNickname
|
||||
? props.nickname
|
||||
: cite(props.ship)}
|
||||
</span>
|
||||
|
@ -161,6 +161,7 @@ export class LinkDetail extends Component {
|
||||
page={props.page}
|
||||
linkIndex={props.linkIndex}
|
||||
time={this.state.data.time}
|
||||
hideNicknames={props.hideNicknames}
|
||||
/>
|
||||
<div className="relative">
|
||||
<div className={'relative ba br1 mt6 mb6 ' + focus}>
|
||||
@ -212,6 +213,8 @@ export class LinkDetail extends Component {
|
||||
linkPage={props.page}
|
||||
linkIndex={props.linkIndex}
|
||||
api={props.api}
|
||||
hideAvatars={props.hideAvatars}
|
||||
hideNicknames={props.hideNicknames}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -96,6 +96,8 @@ export class Links extends Component {
|
||||
resourcePath={props.resourcePath}
|
||||
popout={props.popout}
|
||||
api={props.api}
|
||||
hideNicknames={props.hideNicknames}
|
||||
hideAvatars={props.hideAvatars}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -23,7 +23,7 @@ export class Skeleton extends Component {
|
||||
|
||||
return (
|
||||
<div className={'absolute w-100 ' + popoutWindow} style={{ height: 'calc(100% - 45px)' }}>
|
||||
<div className={'cf w-100 h-100 flex ' + popoutBorder}>
|
||||
<div className={'bg-white bg-gray0-d cf w-100 h-100 flex ' + popoutBorder}>
|
||||
<ChannelsSidebar
|
||||
active={props.active}
|
||||
popout={popout}
|
||||
|
@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { Box, InputLabel, Radio, Input } from '@tlon/indigo-react';
|
||||
|
||||
import GlobalApi from '~/logic/api/global';
|
||||
import { S3State } from '~/types';
|
||||
import { ImageInput } from '~/views/components/ImageInput';
|
||||
|
||||
export type BgType = "none" | "url" | "color";
|
||||
|
||||
export function BackgroundPicker({
|
||||
bgType,
|
||||
bgUrl,
|
||||
api,
|
||||
s3,
|
||||
}: {
|
||||
bgType: BgType;
|
||||
bgUrl?: string;
|
||||
api: GlobalApi;
|
||||
s3: S3State;
|
||||
}) {
|
||||
return (
|
||||
<Box>
|
||||
<InputLabel>Landscape Background</InputLabel>
|
||||
<Box display="flex" alignItems="center">
|
||||
<Box mt={3} mr={7}>
|
||||
<Radio label="Image" id="url" name="bgType" />
|
||||
{bgType === "url" && (
|
||||
<ImageInput
|
||||
api={api}
|
||||
s3={s3}
|
||||
id="bgUrl"
|
||||
name="bgUrl"
|
||||
label="URL"
|
||||
url={bgUrl || ""}
|
||||
/>
|
||||
)}
|
||||
<Radio label="Color" id="color" name="bgType" />
|
||||
{bgType === "color" && (
|
||||
<Input
|
||||
ml={4}
|
||||
type="text"
|
||||
label="Color"
|
||||
id="bgColor"
|
||||
name="bgColor"
|
||||
/>
|
||||
)}
|
||||
<Radio label="None" id="none" name="bgType" />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,102 @@
|
||||
import React, { useCallback } from "react";
|
||||
|
||||
import {
|
||||
Input,
|
||||
Box,
|
||||
Button,
|
||||
Col,
|
||||
Text,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
} from "@tlon/indigo-react";
|
||||
import { Formik, Form } from "formik";
|
||||
|
||||
import GlobalApi from "../../../../api/global";
|
||||
|
||||
export function BucketList({
|
||||
buckets,
|
||||
selected,
|
||||
api,
|
||||
}: {
|
||||
buckets: Set<string>;
|
||||
selected: string;
|
||||
api: GlobalApi;
|
||||
}) {
|
||||
const _buckets = Array.from(buckets);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(values: { newBucket: string }) => {
|
||||
api.s3.addBucket(values.newBucket);
|
||||
},
|
||||
[api]
|
||||
);
|
||||
|
||||
const onSelect = useCallback(
|
||||
(bucket: string) => {
|
||||
return function () {
|
||||
api.s3.setCurrentBucket(bucket);
|
||||
};
|
||||
},
|
||||
[api]
|
||||
);
|
||||
|
||||
const onDelete = useCallback(
|
||||
(bucket: string) => {
|
||||
return function () {
|
||||
api.s3.removeBucket(bucket);
|
||||
};
|
||||
},
|
||||
[api]
|
||||
);
|
||||
|
||||
return (
|
||||
<Formik initialValues={{ newBucket: "" }} onSubmit={onSubmit}>
|
||||
<Form>
|
||||
<Col alignItems="start">
|
||||
{_buckets.map((bucket) => (
|
||||
<Box
|
||||
key={bucket}
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
borderRadius={1}
|
||||
border={1}
|
||||
borderColor="washedGray"
|
||||
fontSize={1}
|
||||
pl={2}
|
||||
mb={2}
|
||||
width="100%"
|
||||
>
|
||||
<Text>{bucket}</Text>
|
||||
{bucket === selected && (
|
||||
<Text p={1} color="green">
|
||||
Active
|
||||
</Text>
|
||||
)}
|
||||
{bucket !== selected && (
|
||||
<Menu>
|
||||
<MenuButton sm>Options</MenuButton>
|
||||
<MenuList>
|
||||
<MenuItem onSelect={onSelect(bucket)}>Make Active</MenuItem>
|
||||
<MenuItem onSelect={onDelete(bucket)}>Delete</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
<Input
|
||||
mt={2}
|
||||
type="text"
|
||||
label="New Bucket"
|
||||
id="newBucket"
|
||||
/>
|
||||
<Button border borderColor="washedGrey" type="submit">
|
||||
Add
|
||||
</Button>
|
||||
</Col>
|
||||
</Form>
|
||||
</Formik>
|
||||
);
|
||||
}
|
@ -0,0 +1,146 @@
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
Box,
|
||||
InputLabel,
|
||||
Checkbox,
|
||||
Button,
|
||||
} from "@tlon/indigo-react";
|
||||
import { Formik, Form } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import _ from "lodash";
|
||||
|
||||
import GlobalApi from "../../../../api/global";
|
||||
import { LaunchState } from "../../../../types/launch-update";
|
||||
import { DropLaunchTiles } from "./DropLaunch";
|
||||
import { S3State, BackgroundConfig } from "../../../../types";
|
||||
import { BackgroundPicker, BgType } from './BackgroundPicker';
|
||||
|
||||
const formSchema = Yup.object().shape({
|
||||
tileOrdering: Yup.array().of(Yup.string()),
|
||||
bgType: Yup.string()
|
||||
.oneOf(["none", "color", "url"], "invalid")
|
||||
.required("Required"),
|
||||
bgUrl: Yup.string().url(),
|
||||
bgColor: Yup.string().matches(/#([A-F]|[a-f]|[0-9]){6}/, "Invalid color"),
|
||||
avatars: Yup.boolean(),
|
||||
nicknames: Yup.boolean(),
|
||||
});
|
||||
|
||||
interface FormSchema {
|
||||
tileOrdering: string[];
|
||||
bgType: BgType;
|
||||
bgColor: string | undefined;
|
||||
bgUrl: string | undefined;
|
||||
avatars: boolean;
|
||||
nicknames: boolean;
|
||||
}
|
||||
|
||||
interface DisplayFormProps {
|
||||
api: GlobalApi;
|
||||
launch: LaunchState;
|
||||
dark: boolean;
|
||||
background: BackgroundConfig;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
s3: S3State;
|
||||
}
|
||||
|
||||
export default function DisplayForm(props: DisplayFormProps) {
|
||||
const {
|
||||
api,
|
||||
launch,
|
||||
background,
|
||||
hideAvatars,
|
||||
hideNicknames,
|
||||
s3
|
||||
} = props;
|
||||
|
||||
let bgColor, bgUrl;
|
||||
if (background?.type === "url") {
|
||||
bgUrl = background.url;
|
||||
}
|
||||
if (background?.type === "color") {
|
||||
bgColor = background.color;
|
||||
}
|
||||
const bgType = background?.type || "none";
|
||||
|
||||
return (
|
||||
<Formik
|
||||
validationSchema={formSchema}
|
||||
initialValues={
|
||||
{
|
||||
bgType,
|
||||
bgColor,
|
||||
bgUrl,
|
||||
avatars: hideAvatars,
|
||||
nicknames: hideNicknames,
|
||||
tileOrdering: launch.tileOrdering,
|
||||
} as FormSchema
|
||||
}
|
||||
onSubmit={(values, actions) => {
|
||||
api.launch.changeOrder(values.tileOrdering);
|
||||
|
||||
const bgConfig: BackgroundConfig =
|
||||
values.bgType === "color"
|
||||
? { type: "color", color: values.bgColor || "" }
|
||||
: values.bgType === "url"
|
||||
? { type: "url", url: values.bgUrl || "" }
|
||||
: undefined;
|
||||
|
||||
api.local.setBackground(bgConfig);
|
||||
api.local.hideAvatars(values.avatars);
|
||||
api.local.hideNicknames(values.nicknames);
|
||||
api.local.dehydrate();
|
||||
actions.setSubmitting(false);
|
||||
}}
|
||||
>
|
||||
{(props) => (
|
||||
<Form>
|
||||
<Box
|
||||
display="grid"
|
||||
gridTemplateColumns="1fr"
|
||||
gridTemplateRows="auto"
|
||||
gridRowGap={3}
|
||||
>
|
||||
<Box color="black" fontSize={1} mb={3} fontWeight={900}>
|
||||
Display Preferences
|
||||
</Box>
|
||||
<Box mb={2}>
|
||||
<InputLabel display="block" pb={2}>
|
||||
Tile Order
|
||||
</InputLabel>
|
||||
<DropLaunchTiles
|
||||
id="tileOrdering"
|
||||
name="tileOrdering"
|
||||
tiles={launch.tiles}
|
||||
order={launch.tileOrdering}
|
||||
/>
|
||||
</Box>
|
||||
<BackgroundPicker
|
||||
bgType={props.values.bgType}
|
||||
bgUrl={props.values.bgUrl}
|
||||
api={api}
|
||||
s3={s3}
|
||||
/>
|
||||
<Box>
|
||||
<Checkbox
|
||||
label="Disable avatars"
|
||||
id="avatars"
|
||||
caption="Do not show user-set avatars"
|
||||
/>
|
||||
<Checkbox
|
||||
label="Disable nicknames"
|
||||
id="nicknames"
|
||||
caption="Do not show user-set nicknames"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Button border={1} borderColor="washedGray" type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
}
|
127
pkg/interface/src/views/apps/profile/components/lib/DragTile.tsx
Normal file
127
pkg/interface/src/views/apps/profile/components/lib/DragTile.tsx
Normal file
@ -0,0 +1,127 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { useDrag } from "react-dnd";
|
||||
import { usePreview } from "react-dnd-multi-backend";
|
||||
import { capitalize } from "lodash";
|
||||
import { TileTypeBasic, Tile } from "../../../../types/launch-update";
|
||||
|
||||
import { Box, Img as _Img, Text } from "@tlon/indigo-react";
|
||||
import styled from "styled-components";
|
||||
|
||||
// Need to change dojo image
|
||||
const Img = styled(_Img)<{ invert?: boolean }>`
|
||||
${(p) =>
|
||||
p.theme.colors.white !== "rgba(255,255,255,1)" ? `filter: invert(1);` : ``}
|
||||
|
||||
${(p) =>
|
||||
!p.invert
|
||||
? ``
|
||||
: p.theme.colors.white !== "rgba(255,255,255,1)"
|
||||
? `
|
||||
filter: invert(0);
|
||||
`
|
||||
: `filter: invert(1);`}
|
||||
`;
|
||||
|
||||
interface DragTileProps {
|
||||
index: number;
|
||||
tile: Tile;
|
||||
title: string;
|
||||
style?: any;
|
||||
}
|
||||
|
||||
function DragTileBox({ title, index, tile, ...props }: any) {
|
||||
const [, dragRef] = useDrag({
|
||||
item: { type: "launchTile", index, tile, title },
|
||||
collect: (monitor) => ({}),
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={dragRef}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="space-around"
|
||||
flexDirection="column"
|
||||
border={1}
|
||||
borderColor="black"
|
||||
height="100%"
|
||||
width="100%"
|
||||
style={{ cursor: "move" }}
|
||||
{...props}
|
||||
></Box>
|
||||
);
|
||||
}
|
||||
|
||||
function DragTileCustom({ index, title, style }: any) {
|
||||
const tile = { type: { custom: null } };
|
||||
return (
|
||||
<DragTileBox
|
||||
bg="white"
|
||||
style={style}
|
||||
title={title}
|
||||
tile={tile}
|
||||
index={index}
|
||||
>
|
||||
<Text fontSize={1}>{capitalize(title)}</Text>
|
||||
</DragTileBox>
|
||||
);
|
||||
}
|
||||
|
||||
function DragTileBasic(props: {
|
||||
tile: TileTypeBasic;
|
||||
index: number;
|
||||
style: any;
|
||||
}) {
|
||||
const { basic: tile } = props.tile;
|
||||
const isDojo = useMemo(() => tile.title === "Dojo", [tile.title]);
|
||||
return (
|
||||
<DragTileBox
|
||||
tile={{ type: props.tile }}
|
||||
index={props.index}
|
||||
bg={
|
||||
"white" // isDojo ? "black" : "white"
|
||||
}
|
||||
style={props.style}
|
||||
>
|
||||
<Img width="48px" height="48px" src={tile.iconUrl} invert={isDojo} />
|
||||
<Text
|
||||
color={
|
||||
"black" // isDojo ? "white" : "black"
|
||||
}
|
||||
>
|
||||
{tile.title}
|
||||
</Text>
|
||||
</DragTileBox>
|
||||
);
|
||||
}
|
||||
|
||||
export function DragTile(props: DragTileProps) {
|
||||
if ("basic" in props.tile.type) {
|
||||
return (
|
||||
<DragTileBasic
|
||||
index={props.index}
|
||||
style={props.style}
|
||||
tile={props.tile.type}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<DragTileCustom
|
||||
style={props.style}
|
||||
title={props.title}
|
||||
index={props.index}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function DragTilePreview() {
|
||||
let { display, style, item } = usePreview();
|
||||
|
||||
if (!display) {
|
||||
return null;
|
||||
}
|
||||
|
||||
style = { ...style, height: "96px", width: "96px", "z-index": "5" };
|
||||
return <DragTile style={style} {...item} />;
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
import React, { useCallback, ReactNode } from "react";
|
||||
import { useDrop } from "react-dnd";
|
||||
import { DndProvider, usePreview } from "react-dnd-multi-backend";
|
||||
import HTML5toTouch from "react-dnd-multi-backend/dist/esm/HTML5toTouch";
|
||||
import { Box } from "@tlon/indigo-react";
|
||||
|
||||
import { DragTile, DragTilePreview } from "./DragTile";
|
||||
import { useField } from "formik";
|
||||
|
||||
function DropLaunchTile({
|
||||
children,
|
||||
index,
|
||||
didDrop,
|
||||
}: {
|
||||
index: number;
|
||||
children: ReactNode;
|
||||
didDrop: (item: number, location: number) => void;
|
||||
}) {
|
||||
const onDrop = useCallback(
|
||||
(item: any, monitor: any) => {
|
||||
didDrop(item.index, index);
|
||||
},
|
||||
[index, didDrop]
|
||||
);
|
||||
|
||||
const { display, style, item } = usePreview();
|
||||
|
||||
const [{ isOver }, drop] = useDrop({
|
||||
accept: "launchTile",
|
||||
drop: onDrop,
|
||||
collect: (monitor) => ({
|
||||
isOver: !!monitor.isOver(),
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={drop}
|
||||
style={{
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DropLaunchTiles({ tiles, name }: any) {
|
||||
const [field, meta, helpers] = useField<string[]>(name);
|
||||
|
||||
const { value } = meta;
|
||||
const { setValue } = helpers;
|
||||
|
||||
const onChange = useCallback(
|
||||
(x: number, y: number) => {
|
||||
// swap tiles
|
||||
let t = value.slice();
|
||||
const c = t[x];
|
||||
t[x] = t[y];
|
||||
t[y] = c;
|
||||
setValue(t);
|
||||
},
|
||||
[setValue, value]
|
||||
);
|
||||
|
||||
return (
|
||||
<DndProvider options={HTML5toTouch}>
|
||||
<Box
|
||||
display="grid"
|
||||
gridGap={2}
|
||||
gridTemplateColumns={["96px 96px", "96px 96px 96px 96px"]}
|
||||
gridAutoRows="96px"
|
||||
>
|
||||
<DragTilePreview />
|
||||
{value.map((tile, i) => (
|
||||
<DropLaunchTile didDrop={onChange} key={`${i}-${tile}`} index={i}>
|
||||
<DragTile title={tile} tile={tiles[tile]} index={i} />
|
||||
</DropLaunchTile>
|
||||
))}
|
||||
</Box>
|
||||
</DndProvider>
|
||||
);
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
import React, { useCallback } from "react";
|
||||
|
||||
import {
|
||||
Input,
|
||||
Box,
|
||||
Button,
|
||||
Col,
|
||||
Text,
|
||||
Menu
|
||||
} from "@tlon/indigo-react";
|
||||
|
||||
import { Formik, Form } from "formik";
|
||||
import GlobalApi from "../../../../api/global";
|
||||
import { BucketList } from './BucketList';
|
||||
import { S3State } from "../../../../types";
|
||||
|
||||
interface FormSchema {
|
||||
s3bucket: string;
|
||||
s3buckets: string[];
|
||||
s3endpoint: string;
|
||||
s3accessKeyId: string;
|
||||
s3secretAccessKey: string;
|
||||
}
|
||||
|
||||
interface S3FormProps {
|
||||
api: GlobalApi;
|
||||
s3: S3State;
|
||||
}
|
||||
|
||||
export default function S3Form(props: S3FormProps) {
|
||||
const { api, s3 } = props;
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(values: FormSchema) => {
|
||||
if (values.s3secretAccessKey !== s3.credentials?.secretAccessKey) {
|
||||
api.s3.setSecretAccessKey(values.s3secretAccessKey);
|
||||
}
|
||||
|
||||
if (values.s3endpoint !== s3.credentials?.endpoint) {
|
||||
api.s3.setEndpoint(values.s3endpoint);
|
||||
}
|
||||
|
||||
if (values.s3accessKeyId !== s3.credentials?.accessKeyId) {
|
||||
api.s3.setAccessKeyId(values.s3accessKeyId);
|
||||
}
|
||||
},
|
||||
[api, s3]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Col>
|
||||
<Box color="black" mb={4} fontSize={1} fontWeight={900}>
|
||||
S3 Credentials
|
||||
</Box>
|
||||
<Formik
|
||||
initialValues={
|
||||
{
|
||||
s3bucket: s3.configuration.currentBucket,
|
||||
s3buckets: Array.from(s3.configuration.buckets),
|
||||
s3endpoint: s3.credentials?.endpoint,
|
||||
s3accessKeyId: s3.credentials?.accessKeyId,
|
||||
s3secretAccessKey: s3.credentials?.secretAccessKey,
|
||||
} as FormSchema
|
||||
}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Form>
|
||||
<Input width="256px" type="text" label="Endpoint" id="s3endpoint" />
|
||||
<Input
|
||||
width="256px"
|
||||
type="text"
|
||||
label="Access Key ID"
|
||||
id="s3accessKeyId"
|
||||
/>
|
||||
<Input
|
||||
width="256px"
|
||||
type="password"
|
||||
label="Secret Access Key"
|
||||
id="s3secretAccessKey"
|
||||
/>
|
||||
<Button border={1} type="submit">
|
||||
Submit
|
||||
</Button>
|
||||
</Form>
|
||||
</Formik>
|
||||
</Col>
|
||||
<Col>
|
||||
<Box color="black" mb={4} fontSize={1} fontWeight={700}>
|
||||
S3 Buckets
|
||||
</Box>
|
||||
<BucketList
|
||||
buckets={s3.configuration.buckets}
|
||||
selected={s3.configuration.currentBucket}
|
||||
api={api}
|
||||
/>
|
||||
</Col>
|
||||
</>
|
||||
);
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
import React from "react";
|
||||
import { Box, Button } from "@tlon/indigo-react";
|
||||
|
||||
import GlobalApi from "../../../../api/global";
|
||||
|
||||
interface SecuritySettingsProps {
|
||||
api: GlobalApi;
|
||||
}
|
||||
|
||||
export default function SecuritySettings({ api }: SecuritySettingsProps) {
|
||||
return (
|
||||
<Box display="grid" gridTemplateRows="auto" gridTemplateColumns="1fr" gridRowGap={2}>
|
||||
<Box color="black" fontSize={1} mb={4} fontWeight={900}>
|
||||
Security
|
||||
</Box>
|
||||
<Box color="black" fontSize={0} fontWeight={700}>
|
||||
Log out of this session
|
||||
</Box>
|
||||
<Box fontSize={0} mt={2} color="gray">
|
||||
You will be logged out of your Urbit on this browser.
|
||||
<form method="post" action="/~/logout">
|
||||
<Button narrow mt='4' border={1}>
|
||||
Logout
|
||||
</Button>
|
||||
</form>
|
||||
</Box>
|
||||
{/* <Box color="black" fontSize={0} mt={4} fontWeight={700}>
|
||||
Log out of all sessions
|
||||
</Box> */}
|
||||
{/* Restore after testing sending 'all' in POST body
|
||||
<Box fontSize={0} mt={2} color="gray">
|
||||
You will be logged out of all browsers that have currently logged into your Urbit.
|
||||
<form method="post" action="/~/logout">
|
||||
<Button error narrow mt={4} border={1}>
|
||||
Logout
|
||||
</Button>
|
||||
</form>
|
||||
</Box> */}
|
||||
</Box>
|
||||
);
|
||||
}
|
58
pkg/interface/src/views/apps/profile/components/settings.tsx
Normal file
58
pkg/interface/src/views/apps/profile/components/settings.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import React from "react";
|
||||
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
Button,
|
||||
Col,
|
||||
Input,
|
||||
InputLabel,
|
||||
Radio,
|
||||
Checkbox,
|
||||
} from "@tlon/indigo-react";
|
||||
import * as Yup from "yup";
|
||||
import { Formik, Form } from "formik";
|
||||
import _ from "lodash";
|
||||
|
||||
import GlobalApi from "../../../api/global";
|
||||
import { StoreState } from "../../../store/type";
|
||||
import DisplayForm from "./lib/DisplayForm";
|
||||
import S3Form from "./lib/S3Form";
|
||||
import SecuritySettings from "./lib/Security";
|
||||
|
||||
type ProfileProps = StoreState & { api: GlobalApi; ship: string };
|
||||
|
||||
export default function Settings({
|
||||
api,
|
||||
launch,
|
||||
s3,
|
||||
dark,
|
||||
hideAvatars,
|
||||
hideNicknames,
|
||||
background,
|
||||
}: ProfileProps) {
|
||||
return (
|
||||
<Box
|
||||
backgroundColor="white"
|
||||
fontSize={2}
|
||||
display="grid"
|
||||
gridTemplateRows="auto"
|
||||
gridTemplateColumns="1fr"
|
||||
gridRowGap={7}
|
||||
p={4}
|
||||
maxWidth="400px"
|
||||
>
|
||||
<DisplayForm
|
||||
api={api}
|
||||
launch={launch}
|
||||
dark={dark}
|
||||
hideNicknames={hideNicknames}
|
||||
hideAvatars={hideAvatars}
|
||||
background={background}
|
||||
s3={s3}
|
||||
/>
|
||||
<S3Form api={api} s3={s3} />
|
||||
<SecuritySettings api={api} />
|
||||
</Box>
|
||||
);
|
||||
}
|
124
pkg/interface/src/views/apps/profile/profile.tsx
Normal file
124
pkg/interface/src/views/apps/profile/profile.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
import React from "react";
|
||||
|
||||
import { Box, Text, Row, Col, Center, Icon } from "@tlon/indigo-react";
|
||||
|
||||
import { Sigil } from "~/logic/lib/sigil";
|
||||
import { uxToHex, MOBILE_BROWSER_REGEX } from "~/logic/lib/util";
|
||||
|
||||
import Settings from "./components/settings";
|
||||
import { Route, Link } from "react-router-dom";
|
||||
import { ContactCard } from "../groups/components/lib/ContactCard";
|
||||
|
||||
const SidebarItem = ({ children, view, current }) => {
|
||||
const selected = current === view;
|
||||
const color = selected ? "blue" : "black";
|
||||
return (
|
||||
<Link to={`/~profile/${view}`}>
|
||||
<Row
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
verticalAlign="middle"
|
||||
py={1}
|
||||
px={3}
|
||||
backgroundColor={selected ? "washedBlue" : "white"}
|
||||
>
|
||||
<Icon mr={2} display="inline-block" icon="Circle" fill={color} />
|
||||
<Text color={color} fontSize={0}>
|
||||
{children}
|
||||
</Text>
|
||||
</Row>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default function ProfileScreen(props: any) {
|
||||
const { ship, dark } = props;
|
||||
return (
|
||||
<Route
|
||||
path={["/~profile/:view", "/~profile"]}
|
||||
render={({ match, history }) => {
|
||||
const { view } = match.params;
|
||||
const contact = props.contacts?.["/~/default"]?.[window.ship];
|
||||
const sigilColor = contact?.color
|
||||
? `#${uxToHex(contact.color)}`
|
||||
: dark
|
||||
? "#FFFFFF"
|
||||
: "#000000";
|
||||
if(!contact) {
|
||||
return null;
|
||||
}
|
||||
if (!view && !MOBILE_BROWSER_REGEX.test(window.navigator.userAgent)) {
|
||||
history.replace("/~profile/settings");
|
||||
}
|
||||
|
||||
return (
|
||||
<Box height="100%" px={[0, 3]} pb={[0, 3]} borderRadius={1}>
|
||||
<Box
|
||||
height="100%"
|
||||
width="100%"
|
||||
display="grid"
|
||||
gridTemplateColumns={["100%", "200px 1fr"]}
|
||||
gridTemplateRows={["48px 1fr", "1fr"]}
|
||||
borderRadius={1}
|
||||
bg="white"
|
||||
border={1}
|
||||
borderColor="washedGray"
|
||||
>
|
||||
<Col
|
||||
display={!view ? "flex" : ["none", "flex"]}
|
||||
alignItems="center"
|
||||
borderRight={1}
|
||||
borderColor="washedGray"
|
||||
>
|
||||
<Box width="100%" borderBottom={1} borderBottomColor="washedGray">
|
||||
<Box
|
||||
mx="auto"
|
||||
bg={sigilColor}
|
||||
borderRadius={8}
|
||||
my={4}
|
||||
height={128}
|
||||
width={128}
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Sigil ship={`~${ship}`} size={80} color={sigilColor} />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box width="100%" py={3}>
|
||||
<SidebarItem current={view} view="identity">
|
||||
Your Identity
|
||||
</SidebarItem>
|
||||
<SidebarItem current={view} view="settings">
|
||||
Ship Settings
|
||||
</SidebarItem>
|
||||
</Box>
|
||||
</Col>
|
||||
<Box
|
||||
display={!view ? "none" : ["flex", "none"]}
|
||||
alignItems="center"
|
||||
px={3}
|
||||
borderBottom={1}
|
||||
borderBottomColor="washedGray"
|
||||
>
|
||||
<Link to="/~profile">{"<- Back"}</Link>
|
||||
</Box>
|
||||
<Box overflowY="auto" flexGrow={1}>
|
||||
{view === "settings" && <Settings {...props} />}
|
||||
|
||||
{view === "identity" && (
|
||||
<ContactCard
|
||||
contact={contact}
|
||||
path="/~/default"
|
||||
api={props.api}
|
||||
s3={props.s3}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}}
|
||||
></Route>
|
||||
);
|
||||
}
|
@ -2,7 +2,7 @@ import React, { useRef, useEffect } from "react";
|
||||
import { Route, Switch, useLocation, withRouter } from "react-router-dom";
|
||||
import { Center, Text } from "@tlon/indigo-react";
|
||||
import _ from "lodash";
|
||||
import Helmet from 'react-helmet';
|
||||
import Helmet from "react-helmet";
|
||||
|
||||
import "./css/custom.css";
|
||||
|
||||
@ -12,7 +12,7 @@ import { JoinScreen } from "./components/lib/Join";
|
||||
import { StoreState } from "~/logic/store/type";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import GlobalSubscription from "~/logic/subscription/global";
|
||||
import {NotebookRoutes} from "./components/lib/NotebookRoutes";
|
||||
import { NotebookRoutes } from "./components/lib/NotebookRoutes";
|
||||
|
||||
type PublishAppProps = StoreState & {
|
||||
api: GlobalApi;
|
||||
@ -55,7 +55,15 @@ export default function PublishApp(props: PublishAppProps) {
|
||||
.reduce((acc, count) => acc + count, 0)
|
||||
.value();
|
||||
|
||||
const { api, groups, sidebarShown, invites, associations } = props;
|
||||
const {
|
||||
api,
|
||||
groups,
|
||||
sidebarShown,
|
||||
invites,
|
||||
associations,
|
||||
hideNicknames,
|
||||
hideAvatars,
|
||||
} = props;
|
||||
|
||||
const active = location.pathname.endsWith("/~publish")
|
||||
? "sidebar"
|
||||
@ -64,7 +72,7 @@ export default function PublishApp(props: PublishAppProps) {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{unreadTotal > 0 ? `(${unreadTotal}) ` : ''}OS1 - Publish</title>
|
||||
<title>{unreadTotal > 0 ? `(${unreadTotal}) ` : ""}OS1 - Publish</title>
|
||||
</Helmet>
|
||||
<Route
|
||||
path={[
|
||||
@ -131,7 +139,6 @@ export default function PublishApp(props: PublishAppProps) {
|
||||
? props.match.params.view
|
||||
: "posts";
|
||||
|
||||
|
||||
const ship = props.match.params.ship || "";
|
||||
const book = props.match.params.notebook || "";
|
||||
|
||||
@ -142,19 +149,21 @@ export default function PublishApp(props: PublishAppProps) {
|
||||
bookGroupPath in contacts ? contacts[bookGroupPath] : {};
|
||||
|
||||
const notebook = notebooks?.[ship]?.[book];
|
||||
return (
|
||||
<NotebookRoutes
|
||||
notebook={notebook}
|
||||
ship={ship}
|
||||
book={book}
|
||||
groups={groups}
|
||||
contacts={contacts}
|
||||
notebookContacts={notebookContacts}
|
||||
sidebarShown={sidebarShown}
|
||||
api={api}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<NotebookRoutes
|
||||
notebook={notebook}
|
||||
ship={ship}
|
||||
book={book}
|
||||
groups={groups}
|
||||
contacts={contacts}
|
||||
notebookContacts={notebookContacts}
|
||||
sidebarShown={sidebarShown}
|
||||
api={api}
|
||||
hideNicknames={hideNicknames}
|
||||
hideAvatars={hideAvatars}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
|
@ -10,7 +10,9 @@ interface AuthorProps {
|
||||
ship: string;
|
||||
date: number;
|
||||
showImage?: boolean;
|
||||
children: ReactNode;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export function Author(props: AuthorProps) {
|
||||
@ -18,14 +20,16 @@ export function Author(props: AuthorProps) {
|
||||
const noSig = ship.slice(1);
|
||||
const contact = noSig in contacts ? contacts[noSig] : null;
|
||||
const color = contact?.color ? `#${uxToHex(contact?.color)}` : "#000000";
|
||||
const name = contact?.nickname || cite(ship);
|
||||
const showAvatar = !props.hideAvatars && contact?.avatar;
|
||||
const showNickname = !props.hideNicknames && contact?.nickname;
|
||||
|
||||
const name = showNickname ? contact?.nickname : cite(ship);
|
||||
const dateFmt = moment(date).fromNow();
|
||||
return (
|
||||
<Row alignItems="center" width="auto">
|
||||
{showImage && (
|
||||
<Box>
|
||||
{contact?.avatar ? (
|
||||
{showAvatar ? (
|
||||
<img src={contact?.avatar} height={24} width={24} className="dib" />
|
||||
) : (
|
||||
<Sigil
|
||||
@ -40,7 +44,7 @@ export function Author(props: AuthorProps) {
|
||||
<Box
|
||||
ml={showImage ? 2 : 0}
|
||||
color="gray"
|
||||
fontFamily={contact?.nickname ? "sans" : "mono"}
|
||||
fontFamily={showNickname ? "sans" : "mono"}
|
||||
>
|
||||
{name}
|
||||
</Box>
|
||||
|
@ -23,6 +23,8 @@ interface CommentItemProps {
|
||||
ship: string;
|
||||
api: GlobalApi;
|
||||
note: NoteId;
|
||||
hideNicknames: boolean;
|
||||
hideAvatars: boolean;
|
||||
}
|
||||
|
||||
export function CommentItem(props: CommentItemProps) {
|
||||
@ -63,6 +65,8 @@ export function CommentItem(props: CommentItemProps) {
|
||||
contacts={contacts}
|
||||
ship={commentData?.author}
|
||||
date={commentData["date-created"]}
|
||||
hideAvatars={props.hideAvatars}
|
||||
hideNicknames={props.hideNicknames}
|
||||
>
|
||||
{!disabled && !editing && (
|
||||
<>
|
||||
|
@ -19,6 +19,8 @@ interface CommentsProps {
|
||||
api: GlobalApi;
|
||||
numComments: number;
|
||||
enabled: boolean;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
}
|
||||
|
||||
export function Comments(props: CommentsProps) {
|
||||
@ -74,6 +76,8 @@ export function Comments(props: CommentsProps) {
|
||||
contacts={props.contacts}
|
||||
ship={ship}
|
||||
pending={true}
|
||||
hideNicknames={props.hideNicknames}
|
||||
hideAvatars={props.hideAvatars}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@ -86,6 +90,8 @@ export function Comments(props: CommentsProps) {
|
||||
book={book}
|
||||
ship={ship}
|
||||
note={note["note-id"]}
|
||||
hideNicknames={props.hideNicknames}
|
||||
hideAvatars={props.hideAvatars}
|
||||
/>
|
||||
))}
|
||||
</Col>
|
||||
|
@ -22,6 +22,8 @@ interface NoteProps {
|
||||
notebook: Notebook;
|
||||
contacts: Contacts;
|
||||
api: GlobalApi;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
}
|
||||
|
||||
export function Note(props: NoteProps & RouteComponentProps) {
|
||||
@ -82,6 +84,8 @@ export function Note(props: NoteProps & RouteComponentProps) {
|
||||
<Text display="block" mb={2}>{note?.title || ""}</Text>
|
||||
<Box display="flex">
|
||||
<Author
|
||||
hideNicknames={props.hideNicknames}
|
||||
hideAvatars={props.hideAvatars}
|
||||
ship={note?.author}
|
||||
contacts={contacts}
|
||||
date={note?.["date-created"]}
|
||||
@ -109,6 +113,8 @@ export function Note(props: NoteProps & RouteComponentProps) {
|
||||
numComments={props.note["num-comments"]}
|
||||
contacts={props.contacts}
|
||||
api={props.api}
|
||||
hideNicknames={props.hideNicknames}
|
||||
hideAvatars={props.hideAvatars}
|
||||
/>
|
||||
)}
|
||||
<Spinner
|
||||
|
@ -13,6 +13,7 @@ interface NotePreviewProps {
|
||||
book: string;
|
||||
note: Note;
|
||||
contact?: Contact;
|
||||
hideNicknames?: boolean;
|
||||
}
|
||||
|
||||
const WrappedBox = styled(Box)`
|
||||
@ -23,7 +24,7 @@ export function NotePreview(props: NotePreviewProps) {
|
||||
const { note, contact } = props;
|
||||
|
||||
let name = note.author;
|
||||
if (contact) {
|
||||
if (contact && !props.hideNicknames) {
|
||||
name = contact.nickname.length > 0 ? contact.nickname : note.author;
|
||||
}
|
||||
if (name === note.author) {
|
||||
@ -51,7 +52,10 @@ export function NotePreview(props: NotePreviewProps) {
|
||||
/>
|
||||
</WrappedBox>
|
||||
<Box color="gray" display="flex">
|
||||
<Box mr={3} fontFamily={contact?.nickname ? "sans" : "mono"}>
|
||||
<Box
|
||||
mr={3}
|
||||
fontFamily={contact?.nickname && !props.hideNicknames ? "sans" : "mono"}
|
||||
>
|
||||
{name}
|
||||
</Box>
|
||||
<Box color={note.read ? "gray" : "green"} mr={3}>
|
||||
|
@ -37,6 +37,7 @@ interface NotebookProps {
|
||||
notebookContacts: Contacts;
|
||||
contacts: Rolodex;
|
||||
groups: Groups;
|
||||
hideNicknames: boolean;
|
||||
}
|
||||
|
||||
export function Notebook(props: NotebookProps & RouteComponentProps) {
|
||||
@ -53,6 +54,7 @@ export function Notebook(props: NotebookProps & RouteComponentProps) {
|
||||
|
||||
const notesList = notebook?.["notes-by-date"] || [];
|
||||
const notes = notebook?.notes || {};
|
||||
const showNickname = contact?.nickname && !props.hideNicknames;
|
||||
|
||||
return (
|
||||
<Box
|
||||
@ -72,8 +74,8 @@ export function Notebook(props: NotebookProps & RouteComponentProps) {
|
||||
<Text> {notebook?.title}</Text>
|
||||
<br />
|
||||
<Text color="lightGray">by </Text>
|
||||
<Text fontFamily={contact?.nickname ? "sans" : "mono"}>
|
||||
{contact?.nickname || ship}
|
||||
<Text fontFamily={showNickname ? "sans" : "mono"}>
|
||||
{showNickname ? contact?.nickname : ship}
|
||||
</Text>
|
||||
</Box>
|
||||
<Row justifyContent={["flex-start", "flex-end"]}>
|
||||
@ -106,6 +108,7 @@ export function Notebook(props: NotebookProps & RouteComponentProps) {
|
||||
host={ship}
|
||||
book={book}
|
||||
contacts={notebookContacts}
|
||||
hideNicknames={props.hideNicknames}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
|
@ -10,6 +10,7 @@ interface NotebookPostsProps {
|
||||
notes: Notes;
|
||||
host: string;
|
||||
book: string;
|
||||
hideNicknames?: boolean;
|
||||
}
|
||||
|
||||
export function NotebookPosts(props: NotebookPostsProps) {
|
||||
@ -28,6 +29,7 @@ export function NotebookPosts(props: NotebookPostsProps) {
|
||||
book={props.book}
|
||||
note={note}
|
||||
contact={props.contacts[note.author.substr(1)]}
|
||||
hideNicknames={props.hideNicknames}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@ -19,6 +19,8 @@ interface NotebookRoutesProps {
|
||||
notebookContacts: Contacts;
|
||||
contacts: Rolodex;
|
||||
groups: Groups;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
}
|
||||
|
||||
export function NotebookRoutes(
|
||||
@ -70,6 +72,8 @@ export function NotebookRoutes(
|
||||
notebook={notebook}
|
||||
note={note}
|
||||
contacts={notebookContacts}
|
||||
hideAvatars={props.hideAvatars}
|
||||
hideNicknames={props.hideNicknames}
|
||||
{...routeProps}
|
||||
/>
|
||||
);
|
||||
|
@ -93,6 +93,7 @@ export function Skeleton(props: SkeletonProps) {
|
||||
return (
|
||||
<Box height="100%" width="100%" px={[0, 3]} pb={[0, 3]}>
|
||||
<Box
|
||||
bg="white"
|
||||
display="flex"
|
||||
border={[0, 1]}
|
||||
borderColor={["washedGray", "washedGray"]}
|
||||
|
64
pkg/interface/src/views/components/ColorInput.tsx
Normal file
64
pkg/interface/src/views/components/ColorInput.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import React from "react";
|
||||
import { useField } from "formik";
|
||||
import styled from "styled-components";
|
||||
import { Col, InputLabel, Row, Box, ErrorMessage } from "@tlon/indigo-react";
|
||||
|
||||
import { uxToHex, hexToUx } from "~/logic/lib/util";
|
||||
|
||||
const Input = styled.input`
|
||||
background-color: ${ p => p.theme.colors.white };
|
||||
color: ${ p => p.theme.colors.black };
|
||||
box-sizing: border-box;
|
||||
border: 1px solid;
|
||||
border-right: none;
|
||||
border-color: ${(p) => p.theme.colors.lightGray};
|
||||
border-top-left-radius: ${(p) => p.theme.radii[2]}px;
|
||||
border-bottom-left-radius: ${(p) => p.theme.radii[2]}px;
|
||||
padding: ${(p) => p.theme.space[2]}px;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
`;
|
||||
|
||||
type ColorInputProps = Parameters<typeof Col>[0] & {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export function ColorInput(props: ColorInputProps) {
|
||||
const { id, label, ...rest } = props;
|
||||
const [{ value }, { error }, { setValue }] = useField(id);
|
||||
|
||||
const hex = value.substr(2).replace('.', '');
|
||||
const padded = hex.padStart(6, '0');
|
||||
|
||||
const onChange = (e: any) => {
|
||||
const { value: newValue } = e.target as HTMLInputElement;
|
||||
const valid = newValue.match(/^(\d|[a-f]|[A-F]){0,6}$/);
|
||||
|
||||
if(!valid) {
|
||||
return;
|
||||
}
|
||||
const result = hexToUx(newValue);
|
||||
setValue(result);
|
||||
};
|
||||
|
||||
return (
|
||||
<Col {...rest}>
|
||||
<InputLabel htmlFor={id}>{label}</InputLabel>
|
||||
<Row mt={2}>
|
||||
<Input onChange={onChange} value={hex} />
|
||||
<Box
|
||||
borderBottomRightRadius={1}
|
||||
borderTopRightRadius={1}
|
||||
border={1}
|
||||
borderLeft={0}
|
||||
borderColor="lightGray"
|
||||
width="32px"
|
||||
alignSelf="stretch"
|
||||
bg={`#${padded}`}
|
||||
/>
|
||||
</Row>
|
||||
<ErrorMessage mt="2">{error}</ErrorMessage>
|
||||
</Col>
|
||||
);
|
||||
}
|
@ -6,6 +6,7 @@ import { TwoPaneApp } from './TwoPaneApp';
|
||||
import LaunchApp from '../apps/launch/app';
|
||||
import DojoApp from '../apps/dojo/app';
|
||||
import GroupsApp from '../apps/groups/app';
|
||||
import Profile from '../apps/profile/profile';
|
||||
import ErrorComponent from './Error';
|
||||
|
||||
|
||||
@ -63,6 +64,16 @@ export const Content = (props) => {
|
||||
{...props} />
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path="/~profile"
|
||||
render={ p => (
|
||||
<Profile
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
|
||||
<Route
|
||||
render={p => (
|
||||
<ErrorComponent
|
||||
|
72
pkg/interface/src/views/components/ImageInput.tsx
Normal file
72
pkg/interface/src/views/components/ImageInput.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import React, { useRef, useCallback, useState } from "react";
|
||||
|
||||
import { Box, Input, Img, Button } from "@tlon/indigo-react";
|
||||
import GlobalApi from "~/api/global";
|
||||
import { useField } from "formik";
|
||||
import { S3State } from "~/types/s3-update";
|
||||
import { useS3 } from "~/logic/lib/useS3";
|
||||
|
||||
type ImageInputProps = Parameters<typeof Box>[0] & {
|
||||
id: string;
|
||||
label: string;
|
||||
s3: S3State;
|
||||
};
|
||||
|
||||
export function ImageInput(props: ImageInputProps) {
|
||||
const { id, label, s3, ...rest } = props;
|
||||
|
||||
const { uploadDefault, canUpload } = useS3(s3);
|
||||
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
const [, , { setValue, setError }] = useField(id);
|
||||
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const onImageUpload = useCallback(async () => {
|
||||
const file = ref.current?.files?.item(0);
|
||||
|
||||
if (!file || !canUpload) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setUploading(true);
|
||||
const url = await uploadDefault(file);
|
||||
setUploading(false);
|
||||
setValue(url);
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
}
|
||||
}, [ref.current, uploadDefault, canUpload, setUploading, setValue]);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
ref.current?.click();
|
||||
}, [ref]);
|
||||
|
||||
return (
|
||||
<Box {...rest} display="flex">
|
||||
<Input disabled={uploading} type="text" label={label} id={id} />
|
||||
{canUpload && (
|
||||
<>
|
||||
<Button
|
||||
ml={1}
|
||||
border={3}
|
||||
borderColor="washedGray"
|
||||
style={{ marginTop: "18px" }}
|
||||
onClick={onClick}
|
||||
>
|
||||
{uploading ? "Uploading" : "Upload"}
|
||||
</Button>
|
||||
<input
|
||||
style={{ display: "none" }}
|
||||
type="file"
|
||||
id="fileElement"
|
||||
ref={ref}
|
||||
accept="image/*"
|
||||
onChange={onImageUpload}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
@ -1,48 +1,40 @@
|
||||
import React from 'react';
|
||||
import { Box, Text } from '@tlon/indigo-react';
|
||||
import React from "react";
|
||||
import { Box, Text } from "@tlon/indigo-react";
|
||||
|
||||
const ReconnectBox = ({ color, children, onClick }) => (
|
||||
<Box
|
||||
ml={2}
|
||||
px={2}
|
||||
py={1}
|
||||
display="flex"
|
||||
color={color}
|
||||
bg="white"
|
||||
alignItems="center"
|
||||
border={1}
|
||||
verticalAlign="middle"
|
||||
lineHeight="0"
|
||||
borderRadius={2}
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Text color={color}>{children}</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const ReconnectButton = ({ connection, subscription }) => {
|
||||
const connectedStatus = connection || 'connected';
|
||||
const connectedStatus = connection || "connected";
|
||||
const reconnect = subscription.restart.bind(subscription);
|
||||
if (connectedStatus === 'disconnected') {
|
||||
if (connectedStatus === "disconnected") {
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
ml={2}
|
||||
px={2}
|
||||
py={1}
|
||||
display='inline-block'
|
||||
color='red'
|
||||
border={1}
|
||||
verticalAlign="middle"
|
||||
lineHeight='0'
|
||||
borderRadius={2}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={reconnect}>
|
||||
<Text color='red'>Reconnect ↻</Text>
|
||||
</Box>
|
||||
</>
|
||||
<ReconnectBox onClick={reconnect} color="red">
|
||||
Reconnect ↻
|
||||
</ReconnectBox>
|
||||
);
|
||||
} else if (connectedStatus === 'reconnecting') {
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
ml={2}
|
||||
px={2}
|
||||
py={1}
|
||||
lineHeight="0"
|
||||
verticalAlign="middle"
|
||||
display='inline-block'
|
||||
color='yellow'
|
||||
border={1}
|
||||
borderRadius={2}>
|
||||
<Text color='yellow'>Reconnecting</Text>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
} else if (connectedStatus === "reconnecting") {
|
||||
return <ReconnectBox color="yellow">Reconnecting</ReconnectBox>;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default ReconnectButton;
|
||||
|
@ -1,102 +1,81 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Row, Box, Text, Icon } from '@tlon/indigo-react';
|
||||
import ReconnectButton from './ReconnectButton';
|
||||
import { StatusBarItem } from './StatusBarItem';
|
||||
import { Sigil } from '~/logic/lib/sigil';
|
||||
|
||||
|
||||
const StatusBar = (props) => {
|
||||
|
||||
const location = useLocation();
|
||||
const atHome = Boolean(location.pathname === '/');
|
||||
|
||||
const display = (!window.location.href.includes('popout/'))
|
||||
? 'grid' : 'none';
|
||||
|
||||
const invites = (props.invites && props.invites['/contacts'])
|
||||
? props.invites['/contacts']
|
||||
: {};
|
||||
|
||||
const Notification = (Object.keys(invites).length > 0)
|
||||
? <Icon size="22px" icon="Bullet"
|
||||
fill="blue" position="absolute"
|
||||
top={'-8px'} right={'7px'}
|
||||
/>
|
||||
: null;
|
||||
|
||||
const metaKey = (window.navigator.platform.includes('Mac')) ? '⌘' : 'Ctrl+';
|
||||
|
||||
const mobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(
|
||||
navigator.userAgent
|
||||
);
|
||||
|
||||
return (
|
||||
<Row
|
||||
height="45px"
|
||||
backgroundColor="white"
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
pt="10px"
|
||||
display={(window.location.href.includes('popout/') ? 'none' : 'flex')}>
|
||||
<Box pl={3} display="inline-block">
|
||||
<Box
|
||||
style={{ cursor: 'pointer' }}
|
||||
display='inline-block'
|
||||
borderRadius={2}
|
||||
verticalAlign="middle"
|
||||
lineHeight="0"
|
||||
color='washedGray'
|
||||
border={1}
|
||||
py="6px"
|
||||
px={2}
|
||||
mr={2}
|
||||
onClick={() => props.history.push('/')}>
|
||||
<Box
|
||||
display={display}
|
||||
width="100%"
|
||||
gridTemplateRows="30px"
|
||||
gridTemplateColumns="3fr 1fr"
|
||||
py={2}
|
||||
px={3}
|
||||
>
|
||||
<Row collapse>
|
||||
{atHome ? null : (
|
||||
<StatusBarItem mr={2} onClick={() => props.history.push('/')}>
|
||||
<img
|
||||
className='invert-d'
|
||||
src='/~landscape/img/icon-home.png'
|
||||
height='11'
|
||||
width='11'
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
border={1}
|
||||
borderRadius={2}
|
||||
color='washedGray'
|
||||
display='inline-block'
|
||||
verticalAlign='middle'
|
||||
lineHeight="0"
|
||||
style={{ cursor: 'pointer' }}
|
||||
py={1}
|
||||
px={2}
|
||||
onClick={() => props.api.local.setOmnibox()}>
|
||||
</StatusBarItem>
|
||||
)}
|
||||
<StatusBarItem mr={2} onClick={() => props.api.local.setOmnibox()}>
|
||||
<Text display='inline-block' style={{ transform: 'rotate(180deg)' }}>
|
||||
↩
|
||||
</Text>
|
||||
<Text ml={2} color='black'>
|
||||
Leap
|
||||
</Text>
|
||||
<Text display={mobile ? 'none' : 'inline-block'} ml={4} color='gray'>
|
||||
<Text display={['none', 'inline']} ml={4} color='gray'>
|
||||
{metaKey}/
|
||||
</Text>
|
||||
</Box>
|
||||
<ReconnectButton
|
||||
connection={props.connection}
|
||||
subscription={props.subscription}
|
||||
/>
|
||||
</Box>
|
||||
<Box position="relative" pr={3} display="inline-block">
|
||||
<Box
|
||||
style={{ cursor: 'pointer' }}
|
||||
display='inline-block'
|
||||
borderRadius={2}
|
||||
color='washedGray'
|
||||
verticalAlign="middle"
|
||||
lineHeight='0'
|
||||
border={1}
|
||||
px={2}
|
||||
py={1}
|
||||
onClick={() => props.history.push('/~groups')}>
|
||||
</StatusBarItem>
|
||||
<StatusBarItem
|
||||
onClick={() => props.history.push('/~groups')}
|
||||
badge={Object.keys(invites).length > 0}>
|
||||
<img
|
||||
className='invert-d v-mid mr1'
|
||||
className='invert-d v-mid'
|
||||
src='/~landscape/img/groups.png'
|
||||
height='15'
|
||||
width='15'
|
||||
/>
|
||||
{Notification}
|
||||
<Text ml={1}>Groups</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Row>
|
||||
<Text display={["none", "inline"]} ml={2}>Groups</Text>
|
||||
</StatusBarItem>
|
||||
<ReconnectButton
|
||||
connection={props.connection}
|
||||
subscription={props.subscription}
|
||||
/>
|
||||
</Row>
|
||||
<Row justifyContent="flex-end" collapse>
|
||||
<StatusBarItem onClick={() => props.history.push('/~profile')}>
|
||||
<Sigil ship={props.ship} size={24} color={"#000000"} classes="dib mix-blend-diff" />
|
||||
<Text ml={2} display={["none", "inline"]} fontFamily="mono">{props.ship}</Text>
|
||||
</StatusBarItem>
|
||||
</Row>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
42
pkg/interface/src/views/components/StatusBarItem.tsx
Normal file
42
pkg/interface/src/views/components/StatusBarItem.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import React, { ReactNode } from "react";
|
||||
import { Row as _Row, Icon } from "@tlon/indigo-react";
|
||||
import styled from "styled-components";
|
||||
|
||||
const Row = styled(_Row)`
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
type StatusBarItemProps = Parameters<typeof Row>[0] & { badge?: boolean };
|
||||
|
||||
export function StatusBarItem({
|
||||
badge,
|
||||
children,
|
||||
...props
|
||||
}: StatusBarItemProps) {
|
||||
return (
|
||||
<Row
|
||||
position="relative"
|
||||
collapse
|
||||
border={1}
|
||||
borderRadius={2}
|
||||
color="washedGray"
|
||||
bg="white"
|
||||
alignItems="center"
|
||||
py={1}
|
||||
px={2}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{badge && (
|
||||
<Icon
|
||||
size="22px"
|
||||
icon="Bullet"
|
||||
fill="blue"
|
||||
position="absolute"
|
||||
top={"-10px"}
|
||||
right={"-12px"}
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user