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:
Matilde Park 2020-08-26 20:35:08 -04:00
commit dd82f3861a
69 changed files with 1934 additions and 331 deletions

View File

@ -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>

View File

@ -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",

View File

@ -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",

View File

@ -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) {

View File

@ -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 });
}

View File

@ -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();
}
}

View 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);
}
}

View File

@ -40,6 +40,9 @@ const commandIndex = function () {
commands.push(obj);
}
});
commands.push(result('Profile', '/~profile', 'profile', null));
return commands;
};

View File

@ -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();
}
}

View 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 };
}

View File

@ -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) => {

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -5,6 +5,10 @@ export default class BaseStore<S extends object> {
this.state = this.initialState();
}
dehydrate() {}
rehydrate() {}
initialState() {
return {} as S;
}

View File

@ -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: {},

View File

@ -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

View 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';

View File

@ -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;

View File

@ -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;
}

View File

@ -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}

View File

@ -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>

View File

@ -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}

View File

@ -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();
}
}}

View File

@ -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}
/>
);

View File

@ -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>

View File

@ -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);

View File

@ -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}

View File

@ -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>

View File

@ -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={

View File

@ -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>
</>
);

View File

@ -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>
);
}

View File

@ -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}

View File

@ -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}

View File

@ -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>
</>
);

View File

@ -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>
);

View File

@ -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>
);
}

View File

@ -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>
);

View File

@ -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}

View File

@ -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}
/>
);
});

View File

@ -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}

View File

@ -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>

View File

@ -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>

View File

@ -96,6 +96,8 @@ export class Links extends Component {
resourcePath={props.resourcePath}
popout={props.popout}
api={props.api}
hideNicknames={props.hideNicknames}
hideAvatars={props.hideAvatars}
/>
);
});

View File

@ -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}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View 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} />;
}

View File

@ -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>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
);
}

View 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>
);
}

View 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>
);
}

View File

@ -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>

View File

@ -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>

View File

@ -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 && (
<>

View File

@ -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>

View File

@ -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

View File

@ -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}>

View File

@ -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>

View File

@ -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}
/>
);
})}

View File

@ -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}
/>
);

View File

@ -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"]}

View 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>
);
}

View File

@ -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

View 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>
);
}

View File

@ -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;

View File

@ -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>
);
};

View 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>
);
}