Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
No results found
Show changes
Showing
with 2076 additions and 845 deletions
<svg width="168" height="48" viewBox="0 0 168 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M100 29.3333C96.0005 29.3333 91.0831 38.7919 90.6671 42.6666C90.1058 47.8866 93.3338 47.9999 93.3338 47.9999H108V34.6666C108 34.6666 103.416 29.3333 100 29.3333Z" fill="#50A5E6"/>
<path d="M98.6551 39.078C100.808 35.3486 101.707 31.8364 100.662 31.2334C99.6178 30.6303 97.0255 33.1646 94.8723 36.894C92.7191 40.6234 91.8204 44.1356 92.865 44.7387C93.9096 45.3418 96.5019 42.8074 98.6551 39.078Z" fill="#1C6399"/>
<path d="M86.7813 0C88.356 0 89.632 1.276 89.632 2.85067C89.632 3.90267 90.5227 17.2827 90.5227 17.2827L83.9307 24V2.85067C83.9307 1.276 85.2067 0 86.7813 0Z" fill="#F9CA55"/>
<path d="M85.167 5.76406C85.167 4.29873 86.303 3.11206 87.7043 3.11206C89.1057 3.11206 90.2417 4.30006 90.2417 5.76406C90.2417 5.76406 90.955 13.6867 91.831 18.2574C93.0017 20.7454 93.6563 24.5001 93.5123 26.5881C93.5843 27.1187 101.012 33.2627 101.012 33.2627C100.638 35.6801 98.0003 41.4694 94.667 44.1414L89.9723 40.2427C86.1257 39.7041 84.0283 35.8014 84.0283 34.5574C84.0283 30.5787 85.167 5.76406 85.167 5.76406V5.76406Z" fill="#FFDC5D"/>
<path d="M68 29.3333C72 29.3333 76.9173 38.7919 77.3333 42.6666C77.8947 47.8866 74.6667 47.9999 74.6667 47.9999H60V34.6666C60 34.6666 64.584 29.3333 68 29.3333Z" fill="#50A5E6"/>
<path d="M75.1331 44.7366C76.1777 44.1335 75.279 40.6213 73.1258 36.8919C70.9726 33.1625 68.3803 30.6281 67.3357 31.2312C66.2911 31.8343 67.1898 35.3465 69.343 39.0759C71.4962 42.8053 74.0885 45.3397 75.1331 44.7366Z" fill="#1C6399"/>
<path d="M81.4146 0.0146466C79.7266 -0.154687 78.3039 1.16665 78.3039 2.81865C78.3039 3.85865 78.0586 16.5773 78.0586 16.5773L83.8599 24L83.9426 2.97998C83.9426 1.50531 82.8826 0.161313 81.4146 0.0146466V0.0146466Z" fill="#F9CA55"/>
<path d="M82.8336 5.76406C82.8336 4.29873 81.6976 3.11206 80.2963 3.11206C78.8949 3.11206 77.7589 4.30006 77.7589 5.76406C77.7589 5.76406 77.0456 13.6867 76.1696 18.2574C74.9989 20.7454 74.3443 24.5001 74.4883 26.5881C74.4163 27.1187 66.9883 33.2627 66.9883 33.2627C67.3629 35.6801 70.0003 41.4694 73.3336 44.1414L78.0283 40.2427C81.8749 39.7041 83.9723 35.8014 83.9723 34.5574C83.9723 30.5787 82.8336 5.76406 82.8336 5.76406V5.76406Z" fill="#FFDC5D"/>
<path d="M83.9443 34.4307C83.3923 34.4307 82.9443 33.9827 82.9443 33.4307V3.72266C82.9443 3.17066 83.3923 2.72266 83.9443 2.72266C84.4963 2.72266 84.9443 3.17066 84.9443 3.72266V33.432C84.9457 33.9827 84.4977 34.4307 83.9443 34.4307V34.4307Z" fill="#F9CA55"/>
</g>
<g clip-path="url(#clip1)">
<path d="M135.501 9.98415C135.352 10.1335 135.238 10.3135 135.144 10.5108L135.133 10.5002L120.178 44.1881L120.193 44.2028C119.916 44.7401 120.38 45.8335 121.33 46.7855C122.281 47.7361 123.374 48.2001 123.912 47.9228L123.925 47.9361L157.613 32.9801L157.602 32.9681C157.798 32.8748 157.978 32.7615 158.129 32.6095C160.212 30.5268 156.834 23.7735 150.588 17.5255C144.338 11.2775 137.585 7.90148 135.501 9.98415V9.98415Z" fill="#DD2E44"/>
<path d="M137.333 16L120.554 43.3413L120.178 44.188L120.193 44.2027C119.916 44.74 120.38 45.8333 121.33 46.7853C121.64 47.0947 121.961 47.3293 122.276 47.528L142.666 22.6667L137.333 16Z" fill="#EA596E"/>
<path d="M150.683 17.4214C156.909 23.6507 160.367 30.2907 158.401 32.2534C156.437 34.2187 149.797 30.7627 143.567 24.5361C137.339 18.3067 133.883 11.6641 135.847 9.70006C137.812 7.73606 144.452 11.1921 150.683 17.4214V17.4214Z" fill="#A0041E"/>
<path d="M144.786 18.1453C144.521 18.36 144.174 18.472 143.808 18.432C142.65 18.3067 141.677 17.904 140.996 17.268C140.274 16.5947 139.918 15.6907 140.016 14.7853C140.186 13.196 141.781 11.7373 144.5 12.0307C145.557 12.144 146.029 11.804 146.045 11.6413C146.064 11.48 145.676 11.0467 144.618 10.932C143.461 10.8067 142.488 10.404 141.805 9.76799C141.084 9.09466 140.726 8.19066 140.825 7.28532C140.998 5.69599 142.592 4.23732 145.308 4.53199C146.078 4.61465 146.485 4.45599 146.657 4.35332C146.794 4.26932 146.849 4.18932 146.854 4.14265C146.87 3.98132 146.488 3.54799 145.428 3.43332C144.696 3.35332 144.165 2.69732 144.246 1.96399C144.325 1.23199 144.98 0.702655 145.714 0.782655C148.43 1.07465 149.678 2.83865 149.506 4.42932C149.333 6.02132 147.74 7.47732 145.021 7.18532C144.25 7.10132 143.848 7.26132 143.674 7.36399C143.537 7.44665 143.481 7.52799 143.476 7.57332C143.458 7.73599 143.844 8.16799 144.904 8.28265C147.62 8.57599 148.868 10.3387 148.696 11.9293C148.524 13.5187 146.93 14.9773 144.213 14.6827C143.442 14.6 143.037 14.76 142.864 14.8613C142.725 14.9467 142.672 15.0267 142.666 15.072C142.649 15.2333 143.034 15.6667 144.093 15.7813C144.824 15.8613 145.356 16.5187 145.274 17.2507C145.237 17.616 145.052 17.932 144.786 18.1453V18.1453Z" fill="#AA8DD8"/>
<path d="M160.881 30.4761C163.512 29.7334 165.327 30.9067 165.759 32.4467C166.191 33.9854 165.255 35.9334 162.625 36.6734C161.599 36.9614 161.291 37.4521 161.332 37.6081C161.377 37.7654 161.899 38.0241 162.923 37.7347C165.552 36.9947 167.367 38.1681 167.799 39.7067C168.233 41.2467 167.295 43.1921 164.664 43.9334C163.639 44.2214 163.329 44.7134 163.375 44.8694C163.419 45.0254 163.939 45.2841 164.964 44.9961C165.671 44.7974 166.409 45.2094 166.608 45.9174C166.805 46.6267 166.393 47.3627 165.684 47.5627C163.056 48.3027 161.24 47.1321 160.805 45.5907C160.373 44.0521 161.311 42.1067 163.943 41.3654C164.969 41.0761 165.277 40.5867 165.232 40.4294C165.189 40.2734 164.669 40.0134 163.645 40.3014C161.013 41.0427 159.2 39.8721 158.767 38.3294C158.333 36.7907 159.271 34.8454 161.901 34.1027C162.925 33.8161 163.233 33.3227 163.191 33.1681C163.145 33.0107 162.627 32.7521 161.601 33.0401C160.892 33.2401 160.157 32.8267 159.957 32.1187C159.759 31.4121 160.172 30.6761 160.881 30.4761V30.4761Z" fill="#77B255"/>
<path d="M150.667 26.88C150.275 26.88 149.889 26.708 149.625 26.38C149.165 25.804 149.259 24.9653 149.833 24.5053C150.123 24.272 157.057 18.8267 166.855 20.228C167.585 20.332 168.091 21.0067 167.987 21.736C167.883 22.464 167.214 22.976 166.478 22.8667C157.821 21.6373 151.562 26.5387 151.501 26.588C151.253 26.7853 150.959 26.88 150.667 26.88V26.88Z" fill="#AA8DD8"/>
<path d="M127.672 21.3335C127.545 21.3335 127.416 21.3148 127.288 21.2775C126.582 21.0655 126.182 20.3228 126.394 19.6175C127.905 14.5868 129.274 6.55879 127.592 4.46545C127.404 4.22812 127.12 3.99479 126.469 4.04412C125.218 4.14012 125.337 6.77879 125.338 6.80545C125.394 7.54012 124.842 8.18012 124.109 8.23479C123.364 8.28012 122.734 7.73879 122.68 7.00412C122.542 5.16545 123.114 1.62412 126.269 1.38545C127.677 1.27879 128.846 1.76812 129.672 2.79479C132.833 6.72945 129.624 18.1361 128.949 20.3841C128.776 20.9615 128.245 21.3335 127.672 21.3335Z" fill="#77B255"/>
<path d="M154 14.6667C155.105 14.6667 156 13.7713 156 12.6667C156 11.5622 155.105 10.6667 154 10.6667C152.895 10.6667 152 11.5622 152 12.6667C152 13.7713 152.895 14.6667 154 14.6667Z" fill="#5C913B"/>
<path d="M122.667 26.6666C124.139 26.6666 125.333 25.4727 125.333 23.9999C125.333 22.5272 124.139 21.3333 122.667 21.3333C121.194 21.3333 120 22.5272 120 23.9999C120 25.4727 121.194 26.6666 122.667 26.6666Z" fill="#9266CC"/>
<path d="M163.333 28C164.438 28 165.333 27.1046 165.333 26C165.333 24.8954 164.438 24 163.333 24C162.228 24 161.333 24.8954 161.333 26C161.333 27.1046 162.228 28 163.333 28Z" fill="#5C913B"/>
<path d="M151.333 44C152.438 44 153.333 43.1046 153.333 42C153.333 40.8954 152.438 40 151.333 40C150.228 40 149.333 40.8954 149.333 42C149.333 43.1046 150.228 44 151.333 44Z" fill="#5C913B"/>
<path d="M157.334 8.00008C158.806 8.00008 160 6.80617 160 5.33341C160 3.86066 158.806 2.66675 157.334 2.66675C155.861 2.66675 154.667 3.86066 154.667 5.33341C154.667 6.80617 155.861 8.00008 157.334 8.00008Z" fill="#FFCC4D"/>
<path d="M163.333 13.3333C164.438 13.3333 165.333 12.4378 165.333 11.3333C165.333 10.2287 164.438 9.33325 163.333 9.33325C162.228 9.33325 161.333 10.2287 161.333 11.3333C161.333 12.4378 162.228 13.3333 163.333 13.3333Z" fill="#FFCC4D"/>
<path d="M159.333 18.6667C160.438 18.6667 161.333 17.7713 161.333 16.6667C161.333 15.5622 160.438 14.6667 159.333 14.6667C158.228 14.6667 157.333 15.5622 157.333 16.6667C157.333 17.7713 158.228 18.6667 159.333 18.6667Z" fill="#FFCC4D"/>
<path d="M130 33.3333C131.105 33.3333 132 32.4378 132 31.3333C132 30.2287 131.105 29.3333 130 29.3333C128.895 29.3333 128 30.2287 128 31.3333C128 32.4378 128.895 33.3333 130 33.3333Z" fill="#FFCC4D"/>
</g>
<path d="M48 24C48 37.2547 37.2547 48 24 48C10.7467 48 0 37.2547 0 24C0 10.7467 10.7467 0 24 0C37.2547 0 48 10.7467 48 24Z" fill="#FFCC4D"/>
<path d="M15.3333 23.9999C17.1743 23.9999 18.6667 20.7167 18.6667 16.6666C18.6667 12.6165 17.1743 9.33325 15.3333 9.33325C13.4924 9.33325 12 12.6165 12 16.6666C12 20.7167 13.4924 23.9999 15.3333 23.9999Z" fill="#664500"/>
<path d="M32.6663 23.9999C34.5073 23.9999 35.9997 20.7167 35.9997 16.6666C35.9997 12.6165 34.5073 9.33325 32.6663 9.33325C30.8254 9.33325 29.333 12.6165 29.333 16.6666C29.333 20.7167 30.8254 23.9999 32.6663 23.9999Z" fill="#664500"/>
<path d="M23.9997 29.3333C19.169 29.3333 15.9637 28.7706 11.9997 27.9999C11.0943 27.8253 9.33301 27.9999 9.33301 30.6666C9.33301 35.9999 15.4597 42.6666 23.9997 42.6666C32.5383 42.6666 38.6663 35.9999 38.6663 30.6666C38.6663 27.9999 36.905 27.8239 35.9997 27.9999C32.0357 28.7706 28.8303 29.3333 23.9997 29.3333Z" fill="#664500"/>
<path d="M12 30.6667C12 30.6667 16 32.0001 24 32.0001C32 32.0001 36 30.6667 36 30.6667C36 30.6667 33.3333 36.0001 24 36.0001C14.6667 36.0001 12 30.6667 12 30.6667Z" fill="white"/>
<defs>
<clipPath id="clip0">
<rect width="48" height="48" fill="white" transform="translate(60)"/>
</clipPath>
<clipPath id="clip1">
<rect width="48" height="48" fill="white" transform="translate(120)"/>
</clipPath>
</defs>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="168" height="48" fill="none" xmlns:v="https://vecta.io/nano"><g clip-path="url(#A)"><path d="M100 29.333c-3.999 0-8.917 9.459-9.333 13.333-.561 5.22 2.667 5.333 2.667 5.333H108V34.667s-4.584-5.333-8-5.333z" fill="#50a5e6"/><path d="M98.655 39.078c2.153-3.729 3.052-7.242 2.007-7.845s-3.637 1.931-5.79 5.661-3.052 7.242-2.007 7.845 3.637-1.931 5.79-5.661z" fill="#1c6399"/><path d="M86.781 0a2.85 2.85 0 0 1 2.851 2.851c0 1.052.891 14.432.891 14.432L83.931 24V2.851A2.85 2.85 0 0 1 86.781 0z" fill="#f9ca55"/><path d="M85.167 5.764c0-1.465 1.136-2.652 2.537-2.652s2.537 1.188 2.537 2.652c0 0 .713 7.923 1.589 12.493 1.171 2.488 1.825 6.243 1.681 8.331.072.531 7.5 6.675 7.5 6.675-.374 2.417-3.012 8.207-6.345 10.879l-4.695-3.899c-3.847-.539-5.944-4.441-5.944-5.685 0-3.979 1.139-28.793 1.139-28.793h0z" fill="#ffdc5d"/><path d="M68 29.333c4 0 8.917 9.459 9.333 13.333.561 5.22-2.667 5.333-2.667 5.333H60V34.667s4.584-5.333 8-5.333z" fill="#50a5e6"/><path d="M75.133 44.737c1.045-.603.146-4.115-2.007-7.845s-4.745-6.264-5.79-5.661-.146 4.115 2.007 7.845 4.745 6.264 5.79 5.661z" fill="#1c6399"/><path d="M81.415.015a2.82 2.82 0 0 0-3.111 2.804c0 1.04-.245 13.759-.245 13.759L83.86 24l.083-21.02c0-1.475-1.06-2.819-2.528-2.965h0z" fill="#f9ca55"/><path d="M82.834 5.764c0-1.465-1.136-2.652-2.537-2.652S77.759 4.3 77.759 5.764c0 0-.713 7.923-1.589 12.493-1.171 2.488-1.825 6.243-1.681 8.331-.072.531-7.5 6.675-7.5 6.675.375 2.417 3.012 8.207 6.345 10.879l4.695-3.899c3.847-.539 5.944-4.441 5.944-5.685 0-3.979-1.139-28.793-1.139-28.793h0z" fill="#ffdc5d"/><path d="M83.944 34.431a1 1 0 0 1-1-1V3.723a1 1 0 1 1 2 0v29.709a1 1 0 0 1-1 .999h0z" fill="#f9ca55"/></g><g clip-path="url(#B)"><path d="M135.501 9.984c-.149.149-.263.329-.357.527l-.011-.011-14.955 33.688.015.015c-.277.537.187 1.631 1.137 2.583s2.044 1.415 2.582 1.137l.013.013 33.688-14.956-.011-.012c.196-.093.376-.207.527-.359 2.083-2.083-1.295-8.836-7.541-15.084s-13.003-9.624-15.087-7.541h0z" fill="#dd2e44"/><path d="M137.333 16l-16.779 27.341-.376.847.015.015c-.277.537.187 1.631 1.137 2.583.31.309.631.544.946.743l20.39-24.861L137.333 16z" fill="#ea596e"/><path d="M150.683 17.421c6.226 6.229 9.684 12.869 7.718 14.832s-8.604-1.491-14.834-7.717-9.684-12.872-7.72-14.836 8.605 1.492 14.836 7.721h0z" fill="#a0041e"/><path d="M144.786 18.145a1.32 1.32 0 0 1-.978.287c-1.158-.125-2.131-.528-2.812-1.164-.722-.673-1.078-1.577-.98-2.483.17-1.589 1.765-3.048 4.484-2.755 1.057.113 1.529-.227 1.545-.389s-.369-.595-1.427-.709c-1.157-.125-2.13-.528-2.813-1.164-.721-.673-1.079-1.577-.98-2.483.173-1.589 1.767-3.048 4.483-2.753.77.083 1.177-.076 1.349-.179.137-.084.192-.164.197-.211.016-.161-.366-.595-1.426-.709-.732-.08-1.263-.736-1.182-1.469a1.33 1.33 0 0 1 1.468-1.181c2.716.292 3.964 2.056 3.792 3.647s-1.766 3.048-4.485 2.756c-.771-.084-1.173.076-1.347.179-.137.083-.193.164-.198.209-.018.163.368.595 1.428.709 2.716.293 3.964 2.056 3.792 3.647s-1.766 3.048-4.483 2.753c-.771-.083-1.176.077-1.349.179-.139.085-.192.165-.198.211-.017.161.368.595 1.427.709.731.08 1.263.737 1.181 1.469-.037.365-.222.681-.488.895h0z" fill="#aa8dd8"/><path d="M160.881 30.476c2.631-.743 4.446.431 4.878 1.971s-.504 3.487-3.134 4.227c-1.026.288-1.334.779-1.293.935s.567.416 1.591.127c2.629-.74 4.444.433 4.876 1.972s-.504 3.485-3.135 4.227c-1.025.288-1.335.78-1.289.936s.564.415 1.589.127c.707-.199 1.445.213 1.644.921s-.215 1.445-.924 1.645c-2.628.74-4.444-.431-4.879-1.972s.506-3.484 3.138-4.225c1.026-.289 1.334-.779 1.289-.936s-.563-.416-1.587-.128c-2.632.741-4.445-.429-4.878-1.972s.504-3.484 3.134-4.227c1.024-.287 1.332-.78 1.29-.935s-.564-.416-1.59-.128c-.709.2-1.444-.213-1.644-.921s.215-1.443.924-1.643h0z" fill="#77b255"/><path d="M150.667 26.88c-.392 0-.778-.172-1.042-.5-.46-.576-.366-1.415.208-1.875.29-.233 7.224-5.679 17.022-4.277.73.104 1.236.779 1.132 1.508s-.773 1.24-1.509 1.131c-8.657-1.229-14.916 3.672-14.977 3.721-.248.197-.542.292-.834.292h0z" fill="#aa8dd8"/><path d="M127.672 21.334a1.37 1.37 0 0 1-.384-.056c-.706-.212-1.106-.955-.894-1.66 1.511-5.031 2.88-13.059 1.198-15.152-.188-.237-.472-.471-1.123-.421-1.251.096-1.132 2.735-1.131 2.761.056.735-.496 1.375-1.229 1.429-.745.045-1.375-.496-1.429-1.231-.138-1.839.434-5.38 3.589-5.619 1.408-.107 2.577.383 3.403 1.409 3.161 3.935-.048 15.341-.723 17.589-.173.577-.704.949-1.277.949z" fill="#77b255"/><path d="M154 14.667a2 2 0 1 0 0-4 2 2 0 1 0 0 4z" fill="#5c913b"/><path d="M122.667 26.667c1.472 0 2.666-1.194 2.666-2.667s-1.194-2.667-2.666-2.667S120 22.527 120 24s1.194 2.667 2.667 2.667z" fill="#9266cc"/><g fill="#5c913b"><path d="M163.333 28a2 2 0 1 0 0-4 2 2 0 1 0 0 4zm-12 16a2 2 0 1 0 0-4 2 2 0 1 0 0 4z"/></g><g fill="#ffcc4d"><path d="M157.334 8C158.806 8 160 6.806 160 5.333s-1.194-2.667-2.666-2.667a2.67 2.67 0 0 0-2.667 2.667A2.67 2.67 0 0 0 157.334 8zm5.999 5.333a2 2 0 1 0 0-4 2 2 0 1 0 0 4zm-4 5.334a2 2 0 0 0 0-4 2 2 0 1 0 0 4zM130 33.333a2 2 0 1 0 0-4 2 2 0 1 0 0 4z"/></g></g><path d="M48 24c0 13.255-10.745 24-24 24S0 37.255 0 24 10.747 0 24 0s24 10.747 24 24z" fill="#ffcc4d"/><path d="M15.333 24c1.841 0 3.333-3.283 3.333-7.333s-1.492-7.333-3.333-7.333S12 12.617 12 16.667 13.492 24 15.333 24zm17.333 0C34.507 24 36 20.717 36 16.667s-1.492-7.333-3.333-7.333-3.333 3.283-3.333 7.333S30.825 24 32.666 24zM24 29.333c-4.831 0-8.036-.563-12-1.333-.905-.175-2.667 0-2.667 2.667 0 5.333 6.127 12 14.667 12s14.667-6.667 14.667-12C38.666 28 36.905 27.824 36 28c-3.964.771-7.169 1.333-12 1.333z" fill="#664500"/><path d="M12 30.667S16 32 24 32s12-1.333 12-1.333S33.333 36 24 36s-12-5.333-12-5.333z" fill="#fff"/><defs><clipPath id="A"><path fill="#fff" transform="translate(60)" d="M0 0h48v48H0z"/></clipPath><clipPath id="B"><path fill="#fff" transform="translate(120)" d="M0 0h48v48H0z"/></clipPath></defs></svg>
\ No newline at end of file
import { observer } from "mobx-react-lite";
import { Channel } from "revolt.js/dist/maps/Channels";
import styled from "styled-components";
import { Text } from "preact-i18n";
import styles from "./Panes.module.scss";
import Button from "../../../components/ui/Button";
import { Channels } from "revolt.js/dist/api/objects";
import InputBox from "../../../components/ui/InputBox";
import { useEffect, useState } from "preact/hooks";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import { useContext, useEffect, useState } from "preact/hooks";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { FileUploader } from "../../../context/revoltjs/FileUploads";
import Button from "../../../components/ui/Button";
import InputBox from "../../../components/ui/InputBox";
interface Props {
channel: Channels.GroupChannel | Channels.TextChannel | Channels.VoiceChannel;
channel: Channel;
}
export function Overview({ channel }: Props) {
const client = useContext(AppContext);
const Row = styled.div`
gap: 20px;
display: flex;
.name {
flex-grow: 1;
input {
width: 100%;
}
}
`;
const [name, setName] = useState(channel.name);
const [description, setDescription] = useState(channel.description ?? '');
export default observer(({ channel }: Props) => {
const [name, setName] = useState(channel.name ?? undefined);
const [description, setDescription] = useState(channel.description ?? "");
useEffect(() => setName(channel.name), [ channel.name ]);
useEffect(() => setDescription(channel.description ?? ''), [ channel.description ]);
useEffect(() => setName(channel.name ?? undefined), [channel.name]);
useEffect(
() => setDescription(channel.description ?? ""),
[channel.description],
);
const [ changed, setChanged ] = useState(false);
const [changed, setChanged] = useState(false);
function save() {
let changes: any = {};
const changes: Record<string, string | undefined> = {};
if (name !== channel.name) changes.name = name;
if (description !== channel.description)
changes.description = description;
client.channels.edit(channel._id, changes);
channel.edit(changes);
setChanged(false);
}
return (
<div className={styles.overview}>
<div className={styles.row}>
<div className="overview">
<Row>
<FileUploader
width={80}
height={80}
......@@ -42,33 +60,44 @@ export function Overview({ channel }: Props) {
fileType="icons"
behaviour="upload"
maxFileSize={2_500_000}
onUpload={icon => client.channels.edit(channel._id, { icon })}
previewURL={client.channels.getIconURL(channel._id, { max_side: 256 }, true)}
remove={() => client.channels.edit(channel._id, { remove: 'Icon' })}
defaultPreview={channel.channel_type === 'Group' ? "/assets/group.png" : undefined}
onUpload={(icon) => channel.edit({ icon })}
previewURL={channel.generateIconURL(
{ max_side: 256 },
true,
)}
remove={() => channel.edit({ remove: "Icon" })}
defaultPreview={
channel.channel_type === "Group"
? "/assets/group.png"
: undefined
}
/>
<div className={styles.name}>
<div className="name">
<h3>
{ channel.channel_type === 'Group' ?
<Text id="app.main.groups.name" /> :
<Text id="app.main.servers.channel_name" /> }
{channel.channel_type === "Group" ? (
<Text id="app.main.groups.name" />
) : (
<Text id="app.main.servers.channel_name" />
)}
</h3>
<InputBox
contrast
value={name}
maxLength={32}
onChange={e => {
setName(e.currentTarget.value)
if (!changed) setChanged(true)
onChange={(e) => {
setName(e.currentTarget.value);
if (!changed) setChanged(true);
}}
/>
</div>
</div>
</Row>
<h3>
{ channel.channel_type === 'Group' ?
<Text id="app.main.groups.description" /> :
<Text id="app.main.servers.channel_description" /> }
{channel.channel_type === "Group" ? (
<Text id="app.main.groups.description" />
) : (
<Text id="app.main.servers.channel_description" />
)}
</h3>
<TextAreaAutoSize
maxRows={10}
......@@ -76,14 +105,16 @@ export function Overview({ channel }: Props) {
maxLength={1024}
value={description}
placeholder={"Add a description..."}
onChange={ev => {
onChange={(ev) => {
setDescription(ev.currentTarget.value);
if (!changed) setChanged(true)
if (!changed) setChanged(true);
}}
/>
<Button onClick={save} contrast disabled={!changed}>
<Text id="app.special.modals.actions.save" />
</Button>
<p>
<Button onClick={save} contrast disabled={!changed}>
<Text id="app.special.modals.actions.save" />
</Button>
</p>
</div>
);
}
});
.overview {
.row {
gap: 20px;
display: flex;
.name {
flex-grow: 1;
input {
width: 100%;
}
}
}
}
import { observer } from "mobx-react-lite";
import {
ChannelPermission,
DEFAULT_PERMISSION_DM,
} from "revolt.js/dist/api/permissions";
import { Channel } from "revolt.js/dist/maps/Channels";
import { useEffect, useState } from "preact/hooks";
import Button from "../../../components/ui/Button";
import Checkbox from "../../../components/ui/Checkbox";
import Tip from "../../../components/ui/Tip";
interface Props {
channel: Channel;
}
// ! FIXME: bad code :)
export default observer(({ channel }: Props) => {
const [selected, setSelected] = useState("default");
type R = { name: string; permissions: number };
const roles: { [key: string]: R } = {};
if (channel.channel_type !== "Group") {
const server = channel.server;
const a = server?.roles ?? {};
for (const b of Object.keys(a)) {
roles[b] = {
name: a[b].name,
permissions: a[b].permissions[1],
};
}
}
const keys = ["default", ...Object.keys(roles)];
const defaultRole = {
name: "Default",
permissions:
(channel.channel_type === "Group"
? channel.permissions
: channel.default_permissions) ?? DEFAULT_PERMISSION_DM,
};
const selectedRole = selected === "default" ? defaultRole : roles[selected];
if (!selectedRole) {
useEffect(() => setSelected("default"), []);
return null;
}
const [p, setPerm] = useState(selectedRole.permissions >>> 0);
useEffect(() => {
setPerm(selectedRole.permissions >>> 0);
}, [selected, selectedRole.permissions]);
return (
<div>
<Tip warning>This section is under construction.</Tip>
<h2>select role</h2>
{selected}
{keys.map((id) => {
const role: R = id === "default" ? defaultRole : roles[id];
return (
<Checkbox
key={id}
checked={selected === id}
onChange={(selected) => selected && setSelected(id)}>
{role.name}
</Checkbox>
);
})}
<h2>channel permissions</h2>
{Object.keys(ChannelPermission).map((perm) => {
if (perm === "View") return null;
const value =
ChannelPermission[perm as keyof typeof ChannelPermission];
if (value & DEFAULT_PERMISSION_DM) {
return (
<Checkbox
checked={(p & value) > 0}
onChange={(c) =>
setPerm(c ? p | value : p ^ value)
}>
{perm}
</Checkbox>
);
}
})}
<Button
contrast
onClick={() => {
channel.setPermissions(selected, p);
}}>
click here to save permissions for role
</Button>
</div>
);
});
import { Text } from "preact-i18n";
import { At, Key, Block } from "@styled-icons/boxicons-regular";
import {
Envelope,
HelpCircle,
Lock,
Trash,
} from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom";
import { Profile } from "revolt-api/types/Users";
import styles from "./Panes.module.scss";
import Tip from "../../../components/ui/Tip";
import Button from "../../../components/ui/Button";
import { Users } from "revolt.js/dist/api/objects";
import { Link, useHistory } from "react-router-dom";
import Overline from "../../../components/ui/Overline";
import { AtSign, Key, Mail } from "@styled-icons/feather";
import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks";
import UserIcon from "../../../components/common/user/UserIcon";
import { useForceUpdate, useSelf } from "../../../context/revoltjs/hooks";
import { stopPropagation } from "../../../lib/stopPropagation";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { ClientStatus, StatusContext } from "../../../context/revoltjs/RevoltClient";
import {
ClientStatus,
StatusContext,
useClient,
} from "../../../context/revoltjs/RevoltClient";
import Tooltip from "../../../components/common/Tooltip";
import UserIcon from "../../../components/common/user/UserIcon";
import Button from "../../../components/ui/Button";
import Tip from "../../../components/ui/Tip";
import CategoryButton from "../../../components/ui/fluent/CategoryButton";
export function Account() {
const { openScreen } = useIntermediate();
export const Account = observer(() => {
const { openScreen, writeClipboard } = useIntermediate();
const status = useContext(StatusContext);
const ctx = useForceUpdate();
const user = useSelf(ctx);
if (!user) return null;
const client = useClient();
const [email, setEmail] = useState("...");
const [profile, setProfile] = useState<undefined | Users.Profile>(
undefined
);
const [revealEmail, setRevealEmail] = useState(false);
const [profile, setProfile] = useState<undefined | Profile>(undefined);
const history = useHistory();
function switchPage(to: string) {
......@@ -32,56 +45,161 @@ export function Account() {
useEffect(() => {
if (email === "..." && status === ClientStatus.ONLINE) {
ctx.client
client
.req("GET", "/auth/user")
.then(account => setEmail(account.email));
.then((account) => setEmail(account.email));
}
if (profile === undefined && status === ClientStatus.ONLINE) {
ctx.client.users
.fetchProfile(user._id)
.then(profile => setProfile(profile ?? {}));
client
.user!.fetchProfile()
.then((profile) => setProfile(profile ?? {}));
}
}, [status]);
}, [client, email, profile, status]);
return (
<div className={styles.user}>
<div className={styles.banner}>
<Link to="/settings/profile">
<UserIcon target={user} size={72} />
</Link>
<div className={styles.username}>@{user.username}</div>
</div>
<div className={styles.details}>
{[
["username", user.username, <AtSign size={24} />],
["email", email, <Mail size={24} />],
["password", "*****", <Key size={24} />]
].map(([field, value, icon]) => (
<div>
{icon}
<div className={styles.detail}>
<Overline>
<Text id={`login.${field}`} />
</Overline>
<p>{value}</p>
</div>
<div>
<Button
onClick={() =>
openScreen({
id: "modify_account",
field: field as any
})
}
contrast
>
<Text id="app.settings.pages.account.change_field" />
</Button>
<div className={styles.container}>
<UserIcon
className={styles.avatar}
target={client.user!}
size={72}
onClick={() => switchPage("profile")}
/>
<div className={styles.userDetail}>
@{client.user!.username}
<div className={styles.userid}>
<Tooltip
content={
<Text id="app.settings.pages.account.unique_id" />
}>
<HelpCircle size={16} />
</Tooltip>
<Tooltip content={<Text id="app.special.copy" />}>
<a
onClick={() =>
writeClipboard(client.user!._id)
}>
{client.user!._id}
</a>
</Tooltip>
</div>
</div>
</div>
<Button onClick={() => switchPage("profile")} contrast>
<Text id="app.settings.pages.profile.edit_profile" />
</Button>
</div>
<div>
{(
[
[
"username",
client.user!.username,
<At key="at" size={24} />,
],
["email", email, <Envelope key="envelope" size={24} />],
["password", "•••••••••", <Key key="key" size={24} />],
] as const
).map(([field, value, icon]) => (
<CategoryButton
key={field}
icon={icon}
description={
field === "email" ? (
revealEmail ? (
<>
{value}{" "}
<a
onClick={(ev) =>
stopPropagation(
ev,
setRevealEmail(false),
)
}>
<Text id="app.special.modals.actions.hide" />
</a>
</>
) : (
<>
•••••••••••@{value.split("@").pop()}{" "}
<a
onClick={(ev) =>
stopPropagation(
ev,
setRevealEmail(true),
)
}>
<Text id="app.special.modals.actions.reveal" />
</a>
</>
)
) : (
value
)
}
account
action="chevron"
onClick={() =>
openScreen({
id: "modify_account",
field,
})
}>
<Text id={`login.${field}`} />
</CategoryButton>
))}
</div>
<h3>
<Text id="app.settings.pages.account.2fa.title" />
</h3>
<h5>
{/*<Text id="app.settings.pages.account.2fa.description" />*/}
Two-factor authentication is currently work-in-progress, see{" "}
{` `}
<a
href="https://gitlab.insrt.uk/insert/rauth/-/issues/2"
target="_blank"
rel="noreferrer">
tracking issue here
</a>
.
</h5>
<CategoryButton
icon={<Lock size={24} color="var(--error)" />}
description={"Set up 2FA Authentication on your account."}
disabled
action="chevron">
Set up Two-factor authentication
</CategoryButton>
<h3>
<Text id="app.settings.pages.account.manage.title" />
</h3>
<h5>
<Text id="app.settings.pages.account.manage.description" />
</h5>
<CategoryButton
icon={<Block size={24} color="var(--error)" />}
description={
"Disable your account. You won't be able to access it unless you log back in."
}
disabled
action={<Text id="general.unavailable" />}>
<Text id="app.settings.pages.account.manage.disable" />
</CategoryButton>
<a href="mailto:contact@revolt.chat?subject=Delete%20my%20account">
<CategoryButton
icon={<Trash size={24} color="var(--error)" />}
description={
"Delete your account, including all of your data."
}
hover
action="external">
<Text id="app.settings.pages.account.manage.delete" />
</CategoryButton>
</a>
<Tip>
<span>
<Text id="app.settings.tips.account.a" />
......@@ -92,4 +210,4 @@ export function Account() {
</Tip>
</div>
);
}
});
import { Text } from "preact-i18n";
import { Reset, Import } from "@styled-icons/boxicons-regular";
import { Pencil } from "@styled-icons/boxicons-solid";
// @ts-expect-error shade-blend-color does not have typings.
import pSBC from "shade-blend-color";
import styles from "./Panes.module.scss";
import { Text } from "preact-i18n";
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import { debounce } from "../../../lib/debounce";
import Button from "../../../components/ui/Button";
import InputBox from "../../../components/ui/InputBox";
import { dispatch } from "../../../redux";
import { connectState } from "../../../redux/connector";
import { WithDispatcher } from "../../../redux/reducers";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import ColourSwatches from "../../../components/ui/ColourSwatches";
import { EmojiPacks, Settings } from "../../../redux/reducers/settings";
import { Theme, ThemeContext, ThemeOptions } from "../../../context/Theme";
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
// @ts-ignore
import pSBC from 'shade-blend-color';
import lightSVG from '../assets/light.svg';
import darkSVG from '../assets/dark.svg';
import {
DEFAULT_FONT,
DEFAULT_MONO_FONT,
Fonts,
FONTS,
FONT_KEYS,
MonospaceFonts,
MONOSPACE_FONTS,
MONOSPACE_FONT_KEYS,
Theme,
ThemeContext,
ThemeOptions,
} from "../../../context/Theme";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import mutantSVG from '../assets/mutant_emoji.svg';
import notoSVG from '../assets/noto_emoji.svg';
import openmojiSVG from '../assets/openmoji_emoji.svg';
import twemojiSVG from '../assets/twemoji_emoji.svg';
import CollapsibleSection from "../../../components/common/CollapsibleSection";
import Tooltip from "../../../components/common/Tooltip";
import Button from "../../../components/ui/Button";
import Checkbox from "../../../components/ui/Checkbox";
import ColourSwatches from "../../../components/ui/ColourSwatches";
import ComboBox from "../../../components/ui/ComboBox";
import InputBox from "../../../components/ui/InputBox";
import darkSVG from "../assets/dark.svg";
import lightSVG from "../assets/light.svg";
import mutantSVG from "../assets/mutant_emoji.svg";
import notoSVG from "../assets/noto_emoji.svg";
import openmojiSVG from "../assets/openmoji_emoji.svg";
import twemojiSVG from "../assets/twemoji_emoji.svg";
interface Props {
settings: Settings;
}
// ! FIXME: code needs to be rewritten to fix jittering
export function Component(props: Props & WithDispatcher) {
export function Component(props: Props) {
const theme = useContext(ThemeContext);
const { writeClipboard, openScreen } = useIntermediate();
function setTheme(theme: ThemeOptions) {
props.dispatcher({
dispatch({
type: "SETTINGS_SET_THEME",
theme
theme,
});
}
function pushOverride(custom: Partial<Theme>) {
props.dispatcher({
const pushOverride = useCallback((custom: Partial<Theme>) => {
dispatch({
type: "SETTINGS_SET_THEME_OVERRIDE",
custom
custom,
});
}
}, []);
function setAccent(accent: string) {
setOverride({
accent,
"sidebar-active": accent,
"scrollbar-thumb": pSBC(-0.2, accent)
"scrollbar-thumb": pSBC(-0.2, accent),
});
}
const emojiPack = props.settings.appearance?.emojiPack ?? 'mutant';
const emojiPack = props.settings.appearance?.emojiPack ?? "mutant";
function setEmojiPack(emojiPack: EmojiPacks) {
props.dispatcher({
type: 'SETTINGS_SET_APPEARANCE',
dispatch({
type: "SETTINGS_SET_APPEARANCE",
options: {
emojiPack
}
emojiPack,
},
});
}
const setOverride = useCallback(debounce(pushOverride, 200), []) as (
custom: Partial<Theme>
) => void;
const [ css, setCSS ] = useState(props.settings.theme?.custom?.css ?? '');
// eslint-disable-next-line react-hooks/exhaustive-deps
const setOverride = useCallback(
debounce(pushOverride as (...args: unknown[]) => void, 200),
[pushOverride],
) as (custom: Partial<Theme>) => void;
const [css, setCSS] = useState(props.settings.theme?.custom?.css ?? "");
useEffect(() => setOverride({ css }), [ css ]);
useEffect(() => setOverride({ css }), [setOverride, css]);
const selected = props.settings.theme?.preset ?? "dark";
return (
......@@ -80,28 +101,45 @@ export function Component(props: Props & WithDispatcher) {
<div className={styles.themes}>
<div className={styles.theme}>
<img
loading="eager"
src={lightSVG}
draggable={false}
data-active={selected === "light"}
onClick={() =>
selected !== "light" &&
setTheme({ preset: "light" })
} />
}
onContextMenu={(e) => e.preventDefault()}
/>
<h4>
<Text id="app.settings.pages.appearance.color.light" />
</h4>
</div>
<div className={styles.theme}>
<img
loading="eager"
src={darkSVG}
draggable={false}
data-active={selected === "dark"}
onClick={() =>
selected !== "dark" && setTheme({ preset: "dark" })
} />
}
onContextMenu={(e) => e.preventDefault()}
/>
<h4>
<Text id="app.settings.pages.appearance.color.dark" />
</h4>
</div>
</div>
{/*<Checkbox
checked={props.settings.theme?.ligatures === true}
onChange={() =>
setTheme({
ligatures: !props.settings.theme?.ligatures,
})
}>
Use the system theme
</Checkbox>*/}
<h3>
<Text id="app.settings.pages.appearance.accent_selector" />
......@@ -130,133 +168,228 @@ export function Component(props: Props & WithDispatcher) {
</Radio>
</div>*/}
<h3>
<Text id="app.settings.pages.appearance.font" />
</h3>
<ComboBox
value={theme.font ?? DEFAULT_FONT}
onChange={(e) =>
pushOverride({ font: e.currentTarget.value as Fonts })
}>
{FONT_KEYS.map((key) => (
<option value={key} key={key}>
{FONTS[key as keyof typeof FONTS].name}
</option>
))}
</ComboBox>
{/* TOFIX: Only show when a font with ligature support is selected, i.e.: Inter.*/}
<p>
<Checkbox
checked={props.settings.theme?.ligatures === true}
onChange={() =>
setTheme({
ligatures: !props.settings.theme?.ligatures,
})
}
description={
<Text id="app.settings.pages.appearance.ligatures_desc" />
}>
<Text id="app.settings.pages.appearance.ligatures" />
</Checkbox>
</p>
<h3>
<Text id="app.settings.pages.appearance.emoji_pack" />
</h3>
<div className={styles.emojiPack}>
<div className={styles.row}>
<div>
<div className={styles.button}
onClick={() => setEmojiPack('mutant')}
data-active={emojiPack === 'mutant'}>
<img src={mutantSVG} draggable={false} />
<div
className={styles.button}
onClick={() => setEmojiPack("mutant")}
data-active={emojiPack === "mutant"}>
<img
loading="eager"
src={mutantSVG}
draggable={false}
onContextMenu={(e) => e.preventDefault()}
/>
</div>
<h4>Mutant Remix <a href="https://mutant.revolt.chat" target="_blank">(by Revolt)</a></h4>
<h4>
Mutant Remix{" "}
<a
href="https://mutant.revolt.chat"
target="_blank"
rel="noreferrer">
(by Revolt)
</a>
</h4>
</div>
<div>
<div className={styles.button}
onClick={() => setEmojiPack('twemoji')}
data-active={emojiPack === 'twemoji'}>
<img src={twemojiSVG} draggable={false} />
<div
className={styles.button}
onClick={() => setEmojiPack("twemoji")}
data-active={emojiPack === "twemoji"}>
<img
loading="eager"
src={twemojiSVG}
draggable={false}
onContextMenu={(e) => e.preventDefault()}
/>
</div>
<h4>Twemoji</h4>
</div>
</div>
<div className={styles.row}>
<div>
<div className={styles.button}
onClick={() => setEmojiPack('openmoji')}
data-active={emojiPack === 'openmoji'}>
<img src={openmojiSVG} draggable={false} />
<div
className={styles.button}
onClick={() => setEmojiPack("openmoji")}
data-active={emojiPack === "openmoji"}>
<img
loading="eager"
src={openmojiSVG}
draggable={false}
onContextMenu={(e) => e.preventDefault()}
/>
</div>
<h4>Openmoji</h4>
</div>
<div>
<div className={styles.button}
onClick={() => setEmojiPack('noto')}
data-active={emojiPack === 'noto'}>
<img src={notoSVG} draggable={false} />
<div
className={styles.button}
onClick={() => setEmojiPack("noto")}
data-active={emojiPack === "noto"}>
<img
loading="eager"
src={notoSVG}
draggable={false}
onContextMenu={(e) => e.preventDefault()}
/>
</div>
<h4>Noto Emoji</h4>
</div>
</div>
</div>
<details>
<summary>
<Text id="app.settings.pages.appearance.advanced" />
<div className={styles.divider}></div>
</summary>
<h3>
<Text id="app.settings.pages.appearance.overrides" />
</h3>
<CollapsibleSection
defaultValue={false}
id="settings_overrides"
summary={<Text id="app.settings.pages.appearance.overrides" />}>
<div className={styles.actions}>
<Button contrast
onClick={() => setTheme({ custom: {} })}>
<Text id="app.settings.pages.appearance.reset_overrides" />
</Button>
<Button contrast
<Tooltip
content={
<Text id="app.settings.pages.appearance.reset_overrides" />
}>
<Button
contrast
iconbutton
onClick={() => setTheme({ custom: {} })}>
<Reset size={22} />
</Button>
</Tooltip>
<div
className={styles.code}
onClick={() => writeClipboard(JSON.stringify(theme))}>
<Text id="app.settings.pages.appearance.export_clipboard" />
</Button>
<Button contrast
onClick={async () => {
const text = await navigator.clipboard.readText();
setOverride(JSON.parse(text));
}}>
<Text id="app.settings.pages.appearance.import_clipboard" />
</Button>
<Button contrast
onClick={async () => {
openScreen({
id: "_input",
question: <Text id="app.settings.pages.appearance.import_theme" />,
field: <Text id="app.settings.pages.appearance.theme_data" />,
callback: async string => setOverride(JSON.parse(string))
});
}}>
<Text id="app.settings.pages.appearance.import_manual" />
</Button>
<Tooltip content={<Text id="app.special.copy" />}>
{" "}
{/*TOFIX: Try to put the tooltip above the .code div without messing up the css challenge */}
{JSON.stringify(theme)}
</Tooltip>
</div>
<Tooltip
content={
<Text id="app.settings.pages.appearance.import" />
}>
<Button
contrast
iconbutton
onClick={async () => {
try {
const text =
await navigator.clipboard.readText();
setOverride(JSON.parse(text));
} catch (err) {
openScreen({
id: "_input",
question: (
<Text id="app.settings.pages.appearance.import_theme" />
),
field: (
<Text id="app.settings.pages.appearance.theme_data" />
),
callback: async (string) =>
setOverride(JSON.parse(string)),
});
}
}}>
<Import size={22} />
</Button>
</Tooltip>
</div>
<h3>App</h3>
<div className={styles.overrides}>
{[
"accent",
"background",
"foreground",
"primary-background",
"primary-header",
"secondary-background",
"secondary-foreground",
"secondary-header",
"tertiary-background",
"tertiary-foreground",
"block",
"message-box",
"mention",
"sidebar-active",
"scrollbar-thumb",
"scrollbar-track",
"status-online",
"status-away",
"status-busy",
"status-streaming",
"status-invisible",
"success",
"warning",
"error",
"hover"
].map(x => (
<div className={styles.entry} key={x}>
{(
[
"accent",
"background",
"foreground",
"primary-background",
"primary-header",
"secondary-background",
"secondary-foreground",
"secondary-header",
"tertiary-background",
"tertiary-foreground",
"block",
"message-box",
"mention",
"scrollbar-thumb",
"scrollbar-track",
"status-online",
"status-away",
"status-busy",
"status-streaming",
"status-invisible",
"success",
"warning",
"error",
"hover",
] as const
).map((x) => (
<div
className={styles.entry}
key={x}
style={{ backgroundColor: theme[x] }}>
<div className={styles.input}>
<input
type="color"
value={theme[x]}
onChange={(v) =>
setOverride({
[x]: v.currentTarget.value,
})
}
/>
</div>
<span>{x}</span>
<div className={styles.override}>
<div className={styles.picker}
style={{ backgroundColor: (theme as any)[x as any] }}>
<input
type="color"
value={(theme as any)[x as any]}
onChange={v =>
setOverride({
[x]: v.currentTarget.value
})
}
/>
<div
className={styles.picker}
onClick={(e) =>
e.currentTarget.parentElement?.parentElement
?.querySelector("input")
?.click()
}>
<Pencil size={24} />
</div>
<InputBox
type="text"
className={styles.text}
value={(theme as any)[x as any]}
onChange={y =>
value={theme[x]}
onChange={(y) =>
setOverride({
[x]: y.currentTarget.value
[x]: y.currentTarget.value,
})
}
/>
......@@ -264,6 +397,34 @@ export function Component(props: Props & WithDispatcher) {
</div>
))}
</div>
</CollapsibleSection>
<CollapsibleSection
id="settings_advanced_appearance"
defaultValue={false}
summary={<Text id="app.settings.pages.appearance.advanced" />}>
<h3>
<Text id="app.settings.pages.appearance.mono_font" />
</h3>
<ComboBox
value={theme.monospaceFont ?? DEFAULT_MONO_FONT}
onChange={(e) =>
pushOverride({
monospaceFont: e.currentTarget
.value as MonospaceFonts,
})
}>
{MONOSPACE_FONT_KEYS.map((key) => (
<option value={key} key={key}>
{
MONOSPACE_FONTS[
key as keyof typeof MONOSPACE_FONTS
].name
}
</option>
))}
</ComboBox>
<h3>
<Text id="app.settings.pages.appearance.custom_css" />
</h3>
......@@ -272,18 +433,15 @@ export function Component(props: Props & WithDispatcher) {
minHeight={480}
code
value={css}
onChange={ev => setCSS(ev.currentTarget.value)} />
</details>
onChange={(ev) => setCSS(ev.currentTarget.value)}
/>
</CollapsibleSection>
</div>
);
}
export const Appearance = connectState(
Component,
state => {
return {
settings: state.settings
};
},
true
);
export const Appearance = connectState(Component, (state) => {
return {
settings: state.settings,
};
});
import { Text } from "preact-i18n";
import styles from "./Panes.module.scss";
import Checkbox from "../../../components/ui/Checkbox";
import { Text } from "preact-i18n";
import { dispatch } from "../../../redux";
import { connectState } from "../../../redux/connector";
import { WithDispatcher } from "../../../redux/reducers";
import { AVAILABLE_EXPERIMENTS, ExperimentOptions } from "../../../redux/reducers/experiments";
import {
AVAILABLE_EXPERIMENTS,
ExperimentOptions,
EXPERIMENTS,
} from "../../../redux/reducers/experiments";
import Checkbox from "../../../components/ui/Checkbox";
interface Props {
options?: ExperimentOptions;
}
export function Component(props: Props & WithDispatcher) {
export function Component(props: Props) {
return (
<div className={styles.notifications}>
<div className={styles.experiments}>
<h3>
<Text id="app.settings.pages.experiments.features" />
</h3>
{
(AVAILABLE_EXPERIMENTS).map(
key =>
<Checkbox
checked={(props.options?.enabled ?? []).indexOf(key) > -1}
onChange={enabled => {
props.dispatcher({
type: enabled ? 'EXPERIMENTS_ENABLE' : 'EXPERIMENTS_DISABLE',
key
});
}}
>
<Text id={`app.settings.pages.experiments.titles.${key}`} />
<p>
<Text id={`app.settings.pages.experiments.descriptions.${key}`} />
</p>
</Checkbox>
)
}
{
AVAILABLE_EXPERIMENTS.length === 0 &&
<Text id="app.settings.pages.experiments.not_available" />
}
{AVAILABLE_EXPERIMENTS.map((key) => (
<Checkbox
key={key}
checked={(props.options?.enabled ?? []).indexOf(key) > -1}
onChange={(enabled) =>
dispatch({
type: enabled
? "EXPERIMENTS_ENABLE"
: "EXPERIMENTS_DISABLE",
key,
})
}
description={EXPERIMENTS[key].description}>
{EXPERIMENTS[key].title}
</Checkbox>
))}
{AVAILABLE_EXPERIMENTS.length === 0 && (
<div className={styles.empty}>
<Text id="app.settings.pages.experiments.not_available" />
</div>
)}
</div>
);
}
export const ExperimentsPage = connectState(
Component,
state => {
return {
options: state.experiments
};
},
true
);
export const ExperimentsPage = connectState(Component, (state) => {
return {
options: state.experiments,
};
});
import { useState } from "preact/hooks";
import styles from "./Panes.module.scss";
import { Localizer, Text } from "preact-i18n";
import Radio from "../../../components/ui/Radio";
import { useState } from "preact/hooks";
import { useClient } from "../../../context/revoltjs/RevoltClient";
import Button from "../../../components/ui/Button";
import InputBox from "../../../components/ui/InputBox";
import Radio from "../../../components/ui/Radio";
import TextArea from "../../../components/ui/TextArea";
import { useSelf } from "../../../context/revoltjs/hooks";
export function Feedback() {
const user = useSelf();
const client = useClient();
const [other, setOther] = useState("");
const [description, setDescription] = useState("");
const [state, setState] = useState<"ready" | "sending" | "sent">("ready");
......@@ -20,19 +22,16 @@ export function Feedback() {
ev.preventDefault();
setState("sending");
await fetch(
`https://workers.revolt.chat/feedback`,
{
method: "POST",
body: JSON.stringify({
checked,
other,
description,
name: user?.username ?? "Unknown User"
}),
mode: 'no-cors'
}
);
await fetch(`https://workers.revolt.chat/feedback`, {
method: "POST",
body: JSON.stringify({
checked,
other,
description,
name: client.user!.username,
}),
mode: "no-cors",
});
setState("sent");
setChecked("Bug");
......@@ -58,15 +57,6 @@ export function Feedback() {
onSelect={() => setChecked("Feature Request")}>
<Text id="app.settings.pages.feedback.feature" />
</Radio>
{ (location.hostname === 'vite.revolt.chat' || location.hostname === 'local.revolt.chat') && <Radio
disabled={state === "sending"}
checked={other === "Revite"}
onSelect={() => {
setChecked("__other_option__");
setOther("Revite");
}}>
Issues with Revite
</Radio> }
<Radio
disabled={state === "sending"}
checked={checked === "__other_option__"}
......@@ -76,11 +66,11 @@ export function Feedback() {
value={other}
disabled={state === "sending"}
name="entry.1151440373.other_option_response"
onChange={e => setOther(e.currentTarget.value)}
onChange={(e) => setOther(e.currentTarget.value)}
placeholder={
(
<Text id="app.settings.pages.feedback.other" />
) as any
) as unknown as string
}
/>
</Localizer>
......@@ -94,11 +84,13 @@ export function Feedback() {
value={description}
id="entry.685672624"
disabled={state === "sending"}
onChange={ev => setDescription(ev.currentTarget.value)}
onChange={(ev) => setDescription(ev.currentTarget.value)}
/>
<Button type="submit" contrast>
<Text id="app.settings.pages.feedback.send" />
</Button>
<p>
<Button type="submit" contrast>
<Text id="app.settings.pages.feedback.send" />
</Button>
</p>
</form>
);
}
import { Text } from "preact-i18n";
import styles from "./Panes.module.scss";
import Tip from "../../../components/ui/Tip";
import { Text } from "preact-i18n";
import { dispatch } from "../../../redux";
import { connectState } from "../../../redux/connector";
import {
Language,
LanguageEntry,
Languages as Langs,
} from "../../../context/Locale";
import Emoji from "../../../components/common/Emoji";
import Checkbox from "../../../components/ui/Checkbox";
import { connectState } from "../../../redux/connector";
import { WithDispatcher } from "../../../redux/reducers";
import { Language, LanguageEntry, Languages as Langs } from "../../../context/Locale";
import Tip from "../../../components/ui/Tip";
import tokiponaSVG from "../assets/toki_pona.svg";
type Props = WithDispatcher & {
type Props = {
locale: Language;
}
};
type Key = [ string, LanguageEntry ];
type Key = [string, LanguageEntry];
function Entry({ entry: [ x, lang ], locale, dispatcher }: { entry: Key } & Props) {
function Entry({ entry: [x, lang], locale }: { entry: Key } & Props) {
return (
<Checkbox
key={x}
className={styles.entry}
checked={locale === x}
onChange={v => {
onChange={(v) => {
if (v) {
dispatcher({
dispatch({
type: "SET_LOCALE",
locale: x as Language
locale: x as Language,
});
}
}}
>
<div className={styles.flag}><Emoji size={42} emoji={lang.emoji} /></div>
<span className={styles.description}>
{lang.display}
</span>
}}>
<div className={styles.flag}>
{lang.emoji === "🙂" ? (
<img src={tokiponaSVG} width={42} />
) : (
<Emoji size={42} emoji={lang.emoji} />
)}
</div>
<span className={styles.description}>{lang.display}</span>
</Checkbox>
);
}
export function Component(props: Props) {
const languages = Object
.keys(Langs)
.map(x => [ x, Langs[x as keyof typeof Langs] ]) as Key[];
const languages = Object.keys(Langs).map((x) => [
x,
Langs[x as keyof typeof Langs],
]) as Key[];
return (
<div className={styles.languages}>
......@@ -48,16 +60,30 @@ export function Component(props: Props) {
</h3>
<div className={styles.list}>
{languages
.filter(([, lang]) => !lang.alt)
.map(([x, lang]) => <Entry key={x} entry={[x, lang]} {...props} />)}
.filter(([, lang]) => !lang.cat)
.map(([x, lang]) => (
<Entry key={x} entry={[x, lang]} {...props} />
))}
</div>
<h3>
<Text id="app.settings.pages.language.const" />
</h3>
<div className={styles.list}>
{languages
.filter(([, lang]) => lang.cat === "const")
.map(([x, lang]) => (
<Entry key={x} entry={[x, lang]} {...props} />
))}
</div>
<h3>
<Text id="app.settings.pages.language.other" />
</h3>
<div className={styles.list}>
{languages
.filter(([, lang]) => lang.alt)
.map(([x, lang]) => <Entry key={x} entry={[x, lang]} {...props} />)}
.filter(([, lang]) => lang.cat === "alt")
.map(([x, lang]) => (
<Entry key={x} entry={[x, lang]} {...props} />
))}
</div>
<Tip>
<span>
......@@ -66,7 +92,7 @@ export function Component(props: Props) {
<a
href="https://weblate.insrt.uk/engage/revolt/?utm_source=widget"
target="_blank"
>
rel="noreferrer">
<Text id="app.settings.tips.languages.b" />
</a>
</Tip>
......@@ -74,12 +100,8 @@ export function Component(props: Props) {
);
}
export const Languages = connectState(
Component,
state => {
return {
locale: state.locale
};
},
true
);
export const Languages = connectState(Component, (state) => {
return {
locale: state.locale,
};
});
import { useEffect, useState } from "preact/hooks";
import Button from "../../../components/ui/Button";
import Checkbox from "../../../components/ui/Checkbox";
export function Native() {
const [config, setConfig] = useState(window.native.getConfig());
const [autoStart, setAutoStart] = useState<boolean | undefined>();
const fetchValue = () => window.native.getAutoStart().then(setAutoStart);
const [hintReload, setHintReload] = useState(false);
const [hintRelaunch, setHintRelaunch] = useState(false);
const [confirmDev, setConfirmDev] = useState(false);
useEffect(() => {
fetchValue();
}, []);
return (
<div>
<h3>App Behavior</h3>
<h5>Some options might require a restart.</h5>
<Checkbox
checked={autoStart ?? false}
disabled={typeof autoStart === "undefined"}
onChange={async (v) => {
if (v) {
await window.native.enableAutoStart();
} else {
await window.native.disableAutoStart();
}
setAutoStart(v);
}}
description="Launch Revolt when you log into your computer.">
Start with computer
</Checkbox>
<Checkbox
checked={config.discordRPC}
onChange={(discordRPC) => {
window.native.set("discordRPC", discordRPC);
setConfig({
...config,
discordRPC,
});
}}
description="Rep Revolt on your Discord status.">
Enable Discord status
</Checkbox>
<Checkbox
checked={config.build === "nightly"}
onChange={(nightly) => {
const build = nightly ? "nightly" : "stable";
window.native.set("build", build);
setHintReload(true);
setConfig({
...config,
build,
});
}}
description="Use the beta branch of Revolt.">
Revolt Nightly
</Checkbox>
<h3>Titlebar</h3>
<Checkbox
checked={!config.frame}
onChange={(frame) => {
window.native.set("frame", !frame);
setHintRelaunch(true);
setConfig({
...config,
frame: !frame,
});
}}
description={<>Let Revolt use its own window frame.</>}>
Custom window frame
</Checkbox>
<Checkbox //FIXME: In Titlebar.tsx, enable .quick css
disabled={true}
checked={!config.frame}
onChange={(frame) => {
window.native.set("frame", !frame);
setHintRelaunch(true);
setConfig({
...config,
frame: !frame,
});
}}
description="Show mute/deafen buttons on the titlebar.">
Enable quick action buttons
</Checkbox>
<h3>Advanced</h3>
<Checkbox
checked={config.hardwareAcceleration}
onChange={async (hardwareAcceleration) => {
window.native.set(
"hardwareAcceleration",
hardwareAcceleration,
);
setHintRelaunch(true);
setConfig({
...config,
hardwareAcceleration,
});
}}
description="Uses your GPU to render the app, disable if you run into visual issues.">
Hardware Acceleration
</Checkbox>
<p style={{ display: "flex", gap: "8px" }}>
<Button
contrast
compact
disabled={!hintReload}
onClick={window.native.reload}>
Reload Page
</Button>
<Button
contrast
compact
disabled={!hintRelaunch}
onClick={window.native.relaunch}>
Reload App
</Button>
</p>
<h3 style={{ marginTop: "4em" }}>Local Development Mode</h3>
{config.build === "dev" ? (
<>
<h5>Development mode is currently on.</h5>
<Button
contrast
compact
onClick={() => {
window.native.set("build", "stable");
window.native.reload();
}}>
Exit Development Mode
</Button>
</>
) : (
<>
<Checkbox
checked={confirmDev}
onChange={setConfirmDev}
description={
<>
This will change the app to the 'dev' branch,
instead loading the app from a local server on
your machine.
<br />
<b>
Without a server running,{" "}
<span style={{ color: "var(--error)" }}>
the app will not load!
</span>
</b>
</>
}>
I understand there's no going back.
</Checkbox>
<p>
<Button
error
compact
disabled={!confirmDev}
onClick={() => {
window.native.set("build", "dev");
window.native.reload();
}}>
Enter Development Mode
</Button>
</p>
</>
)}
</div>
);
}
import { Text } from "preact-i18n";
import styles from "./Panes.module.scss";
import defaultsDeep from "lodash.defaultsdeep";
import Checkbox from "../../../components/ui/Checkbox";
import { connectState } from "../../../redux/connector";
import { WithDispatcher } from "../../../redux/reducers";
import { SOUNDS_ARRAY } from "../../../assets/sounds/Audio";
import styles from "./Panes.module.scss";
import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks";
import { urlBase64ToUint8Array } from "../../../lib/conversion";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { dispatch } from "../../../redux";
import { connectState } from "../../../redux/connector";
import {
DEFAULT_SOUNDS,
NotificationOptions,
SoundOptions,
} from "../../../redux/reducers/settings";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { DEFAULT_SOUNDS, NotificationOptions, SoundOptions } from "../../../redux/reducers/settings";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import Checkbox from "../../../components/ui/Checkbox";
import { SOUNDS_ARRAY } from "../../../assets/sounds/Audio";
interface Props {
options?: NotificationOptions;
}
export function Component({ options, dispatcher }: Props & WithDispatcher) {
export function Component({ options }: Props) {
const client = useContext(AppContext);
const { openScreen } = useIntermediate();
const [pushEnabled, setPushEnabled] = useState<undefined | boolean>(
undefined
undefined,
);
// Load current state of pushManager.
useEffect(() => {
navigator.serviceWorker?.getRegistration().then(async registration => {
const sub = await registration?.pushManager?.getSubscription();
setPushEnabled(sub !== null && sub !== undefined);
});
navigator.serviceWorker
?.getRegistration()
.then(async (registration) => {
const sub = await registration?.pushManager?.getSubscription();
setPushEnabled(sub !== null && sub !== undefined);
});
}, []);
const enabledSounds: SoundOptions = defaultsDeep(options?.sounds ?? {}, DEFAULT_SOUNDS);
const enabledSounds: SoundOptions = defaultsDeep(
options?.sounds ?? {},
DEFAULT_SOUNDS,
);
return (
<div className={styles.notifications}>
<h3>
......@@ -39,41 +54,45 @@ export function Component({ options, dispatcher }: Props & WithDispatcher) {
<Checkbox
disabled={!("Notification" in window)}
checked={options?.desktopEnabled ?? false}
onChange={async desktopEnabled => {
description={
<Text id="app.settings.pages.notifications.descriptions.enable_desktop" />
}
onChange={async (desktopEnabled) => {
if (desktopEnabled) {
let permission = await Notification.requestPermission();
const permission =
await Notification.requestPermission();
if (permission !== "granted") {
return openScreen({
id: "error",
error: "DeniedNotification"
error: "DeniedNotification",
});
}
}
dispatcher({
dispatch({
type: "SETTINGS_SET_NOTIFICATION_OPTIONS",
options: { desktopEnabled }
options: { desktopEnabled },
});
}}
>
}}>
<Text id="app.settings.pages.notifications.enable_desktop" />
<p>
<Text id="app.settings.pages.notifications.descriptions.enable_desktop" />
</p>
</Checkbox>
<Checkbox
disabled={typeof pushEnabled === "undefined"}
checked={pushEnabled ?? false}
onChange={async pushEnabled => {
description={
<Text id="app.settings.pages.notifications.descriptions.enable_push" />
}
onChange={async (pushEnabled) => {
try {
const reg = await navigator.serviceWorker?.getRegistration();
const reg =
await navigator.serviceWorker?.getRegistration();
if (reg) {
if (pushEnabled) {
const sub = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(
client.configuration!.vapid
)
client.configuration!.vapid,
),
});
// tell the server we just subscribed
......@@ -81,12 +100,16 @@ export function Component({ options, dispatcher }: Props & WithDispatcher) {
if (json.keys) {
client.req("POST", "/push/subscribe", {
endpoint: sub.endpoint,
...json.keys
} as any);
...(json.keys as {
p256dh: string;
auth: string;
}),
});
setPushEnabled(true);
}
} else {
const sub = await reg.pushManager.getSubscription();
const sub =
await reg.pushManager.getSubscription();
sub?.unsubscribe();
setPushEnabled(false);
......@@ -94,47 +117,40 @@ export function Component({ options, dispatcher }: Props & WithDispatcher) {
}
}
} catch (err) {
console.error('Failed to enable push!', err);
console.error("Failed to enable push!", err);
}
}}
>
}}>
<Text id="app.settings.pages.notifications.enable_push" />
<p>
<Text id="app.settings.pages.notifications.descriptions.enable_push" />
</p>
</Checkbox>
<h3>
<Text id="app.settings.pages.notifications.sounds" />
</h3>
{
SOUNDS_ARRAY.map(key =>
<Checkbox
checked={enabledSounds[key] ? true : false}
onChange={enabled =>
dispatcher({
type: "SETTINGS_SET_NOTIFICATION_OPTIONS",
options: {
sounds: {
...options?.sounds,
[key]: enabled
}
}
})
}>
<Text id={`app.settings.pages.notifications.sound.${key}`} />
</Checkbox>
)
}
{SOUNDS_ARRAY.map((key) => (
<Checkbox
key={key}
checked={!!enabledSounds[key]}
onChange={(enabled) =>
dispatch({
type: "SETTINGS_SET_NOTIFICATION_OPTIONS",
options: {
sounds: {
...options?.sounds,
[key]: enabled,
},
},
})
}>
<Text
id={`app.settings.pages.notifications.sound.${key}`}
/>
</Checkbox>
))}
</div>
);
}
export const Notifications = connectState(
Component,
state => {
return {
options: state.settings.notification
};
},
true
);
export const Notifications = connectState(Component, (state) => {
return {
options: state.settings.notification,
};
});
.user {
.banner {
gap: 24px;
position: relative;
margin-top: 8px;
margin-bottom: 15px;
gap: 16px;
width: 100%;
padding: 1em;
padding: 12px 10px;
display: flex;
border-radius: 6px;
overflow: hidden;
align-items: center;
background: var(--secondary-header);
border-radius: var(--border-radius);
.username {
font-size: 24px;
.container {
display: flex;
gap: 24px;
align-items: center;
flex-direction: row;
width: 100%;
}
a {
.userDetail {
display: flex;
flex-grow: 1;
gap: 2px;
flex-direction: column;
font-size: 1.5rem;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.avatar {
cursor: pointer;
transition: 0.2s ease filter;
&:hover {
filter: brightness(80%);
}
}
a:hover {
filter: brightness(80%);
.userid {
font-size: 12px;
font-weight: 600;
display: flex;
align-items: center;
gap: 4px;
color: var(--tertiary-foreground);
a {
color: inherit;
cursor: pointer;
}
}
}
.details {
display: flex;
margin-top: 1em;
padding: 1em 0;
gap: 10px;
flex-direction: column;
/*border-top: 1px solid var(--secondary-header);
border-width: 100%;*/
> div {
gap: 12px;
padding: 4px;
/*padding: 4px;*/
padding: 8px 12px;
display: flex;
align-items: center;
flex-direction: row;
}
background: var(--secondary-header);
border-radius: 6px;
.detail {
flex-grow: 1;
> svg {
flex-shrink: 0;
}
}
p {
margin: 0;
font-size: 1rem;
color: var(--tertiary-foreground);
}
}
......@@ -49,7 +91,7 @@
display: grid;
place-items: center;
grid-template-columns: minmax(auto, 100%);
> div {
width: 100%;
max-width: 560px;
......@@ -70,25 +112,45 @@
flex-grow: 1;
}
}
.buttons {
display: flex;
gap: 12px;
}
}
@media only screen and (max-width: 800px) {
.user {
.banner {
gap: 18px;
padding: 0;
flex-direction: column;
> button {
width: 100%;
}
}
}
}
.appearance {
.theme {
min-width: 0;
display: flex;
flex-direction: column;
width: 100%;
}
.themes {
gap: 8px;
width: 100%;
display: flex;
width: 100%;
img {
cursor: pointer;
border-radius: 8px;
border-radius: var(--border-radius);
transition: border 0.3s;
border: 3px solid transparent;
width: 100%;
&[data-active="true"] {
cursor: default;
......@@ -105,29 +167,13 @@
}
details {
font-size: 14px;
font-weight: 700;
text-transform: uppercase;
color: var(--secondary-foreground);
summary {
font-size: 0.8125rem;
font-weight: 700;
text-transform: uppercase;
color: var(--secondary-foreground);
cursor: pointer;
}
/*summary {
display: flex;
flex-grow: 1;
&::after {
display: flex;
align-items: flex-end;
content: "gh";
}
}*/
/*summary::-webkit-details-marker,
summary::marker {
content: "";
}*/
}
.emojiPack {
......@@ -147,15 +193,15 @@
}
.button {
padding: 2rem 1.5rem;
padding: 2rem 1.2rem;
display: grid;
place-items: center;
cursor: pointer;
border-radius: 8px;
transition: border 0.3s;
background: var(--hover);
border: 3px solid transparent;
border-radius: var(--border-radius);
img {
max-width: 100%;
......@@ -188,6 +234,12 @@
text-decoration: underline;
}
}
@media only screen and (max-width: 800px) {
a {
display: block;
}
}
}
}
......@@ -200,56 +252,92 @@
.actions {
gap: 8px;
display: flex;
flex-wrap: wrap;
margin-bottom: 8px;
margin: 18px 0 8px 0;
.code {
cursor: pointer;
display: flex;
align-items: center;
font-size: 0.875rem;
min-width: 0;
flex-grow: 1;
padding: 8px;
font-family: var(--codeblock-font);
border-radius: var(--border-radius);
background: var(--secondary-background);
> div {
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
.overrides {
row-gap: 8px;
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: 16px;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
margin-bottom: 20px;
.entry {
gap: 8px;
padding: 2px;
padding: 12px;
margin-top: 8px;
.override {
display: flex;
}
border: 1px solid black;
border-radius: var(--border-radius);
span {
flex: 1;
display: block;
font-size: 14px;
font-weight: 600;
margin-bottom: 4px;
font-size: 0.875rem;
margin-bottom: 8px;
text-transform: capitalize;
color: transparent;
background: inherit;
background-clip: text;
-webkit-background-clip: text;
filter: sepia(1) invert(1) contrast(9) grayscale(1);
}
.picker {
width: 30px;
height: 30px;
flex-shrink: 0;
border-radius: 4px;
overflow: hidden;
margin-right: 4px;
.override {
gap: 8px;
display: flex;
//TOFIX - Looks wonky on Chromium
border: 1px solid black;
.picker {
width: 38px;
height: 38px;
display: grid;
cursor: pointer;
place-items: center;
border-radius: var(--border-radius);
background: var(--primary-background);
}
input[type="text"] {
width: 0;
min-width: 0;
flex-grow: 1;
}
}
.input {
width: 0;
height: 0;
position: relative;
input {
opacity: 0;
width: 30px;
height: 30px;
border: none;
display: block;
cursor: pointer;
}
}
position: relative;
.text {
border-radius: 4px;
padding: 0 4px 0;
top: 48px;
}
}
}
}
......@@ -258,20 +346,39 @@
.sessions {
.session {
display: flex;
align-items: center;
gap: 12px;
flex-direction: row;
.detail {
display: flex;
gap: 12px;
flex-grow: 1;
svg {
margin-top: 1px;
}
}
}
.entry {
margin: 8px 0;
padding: 16px;
display: flex;
border-radius: 6px;
margin: 10px 0;
flex-direction: column;
border-radius: var(--border-radius);
background: var(--secondary-header);
&[data-active="true"] {
color: var(--primary-background);
background: var(--accent);
margin-bottom: 20px;
.session .detail .info > input {
&:focus {
border-bottom: 2px solid var(--primary-background);
}
}
}
&[data-deleting="true"] {
......@@ -280,27 +387,33 @@
.name {
font-weight: 600;
border-bottom: 2px solid transparent;
}
.icon {
gap: 8px;
display: flex;
padding-right: 12px;
align-items: center;
input {
background: transparent;
border: 0;
font-family: inherit;
font-size: 1rem;
padding: 0;
outline: 0;
border-radius: 0;
color: inherit;
width: 100%;
svg {
height: 42px;
&:focus {
border-bottom: 2px solid var(--accent);
}
div svg {
height: 24px;
&[data-active="true"] {
border-bottom: 2px solid inherit;
}
}
.label {
margin: 0 0 6px 0;
margin-bottom: 8px;
color: var(--primary-text);
font-size: 12px;
font-size: 0.75rem;
font-weight: 600;
}
......@@ -310,56 +423,83 @@
flex-direction: column;
justify-content: center;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
.name {
text-transform: capitalize;
text-overflow: ellipsis;
}
.time {
font-size: 12px;
font-size: 0.75rem;
color: var(--teriary-text);
text-overflow: ellipsis;
overflow: hidden;
}
}
}
}
.notifications {
label {
margin-top: 12px;
> button {
margin-top: 20px;
}
p {
margin-top: 0;
font-size: 0.9em;
color: var(--secondary-foreground);
@media only screen and (max-width: 800px) {
.session {
align-items: unset;
flex-direction: column;
gap: 20px;
> button {
width: 100%;
}
}
> button {
width: 100%;
}
}
}
.languages {
.list {
display: flex;
flex-direction: column;
margin-bottom: 1em;
gap: 8px;
.entry {
padding: 2px 8px;
height: 50px;
border-radius: 4px;
display: flex;
height: 45px;
padding: 0 8px;
background: var(--secondary-header);
border-radius: var(--border-radius);
margin-top: 0;
&:hover {
background: var(--secondary-background);
}
}
.entry > span > span {
gap: 8px;
gap: 12px;
display: flex;
align-items: center;
flex-direction: row;
.flag {
display: flex;
font-size: 42px;
line-height: 48px;
> div {
display: flex;
align-items: center;
justify-content: center;
}
> img {
height: 32px !important;
}
}
.description {
......@@ -374,3 +514,17 @@
display: flex;
flex-direction: column;
}
.experiments {
height: calc(100% - 40px);
.empty {
display: flex;
justify-content: center;
align-items: center;
}
}
section {
margin-bottom: 20px;
}
import { Profile as ProfileI } from "revolt-api/types/Users";
import styles from "./Panes.module.scss";
import Button from "../../../components/ui/Button";
import { Users } from "revolt.js/dist/api/objects";
import { IntlContext, Text, translate } from "preact-i18n";
import { Text } from "preact-i18n";
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import { useContext, useEffect, useState } from "preact/hooks";
import { FileUploader } from "../../../context/revoltjs/FileUploads";
import { useForceUpdate, useSelf } from "../../../context/revoltjs/hooks";
import { useTranslation } from "../../../lib/i18n";
import { UserProfile } from "../../../context/intermediate/popovers/UserProfile";
import { ClientStatus, StatusContext } from "../../../context/revoltjs/RevoltClient";
import { FileUploader } from "../../../context/revoltjs/FileUploads";
import {
ClientStatus,
StatusContext,
useClient,
} from "../../../context/revoltjs/RevoltClient";
import AutoComplete, {
useAutoComplete,
} from "../../../components/common/AutoComplete";
import Button from "../../../components/ui/Button";
export function Profile() {
const { intl } = useContext(IntlContext) as any;
const status = useContext(StatusContext);
const translate = useTranslation();
const client = useClient();
const ctx = useForceUpdate();
const user = useSelf();
if (!user) return null;
const [profile, setProfile] = useState<undefined | Users.Profile>(
undefined
);
const [profile, setProfile] = useState<undefined | ProfileI>(undefined);
// ! FIXME: temporary solution
// ! we should just announce profile changes through WS
function refreshProfile() {
ctx.client.users
.fetchProfile(user!._id)
.then(profile => setProfile(profile ?? {}));
}
const refreshProfile = useCallback(() => {
client
.user!.fetchProfile()
.then((profile) => setProfile(profile ?? {}));
}, [client.user, setProfile]);
useEffect(() => {
if (profile === undefined && status === ClientStatus.ONLINE) {
refreshProfile();
}
}, [status]);
}, [profile, status, refreshProfile]);
const [ changed, setChanged ] = useState(false);
const [changed, setChanged] = useState(false);
function setContent(content?: string) {
setProfile({ ...profile, content });
if (!changed) setChanged(true);
}
const {
onChange,
onKeyUp,
onKeyDown,
onFocus,
onBlur,
...autoCompleteProps
} = useAutoComplete(setContent, {
users: { type: "all" },
});
return (
<div className={styles.user}>
......@@ -44,10 +65,9 @@ export function Profile() {
</h3>
<div className={styles.preview}>
<UserProfile
user_id={user._id}
user_id={client.user!._id}
dummy={true}
dummyProfile={profile}
onClose={() => {}}
/>
</div>
<div className={styles.row}>
......@@ -62,10 +82,16 @@ export function Profile() {
fileType="avatars"
behaviour="upload"
maxFileSize={4_000_000}
onUpload={avatar => ctx.client.users.editUser({ avatar })}
remove={() => ctx.client.users.editUser({ remove: 'Avatar' })}
defaultPreview={ctx.client.users.getAvatarURL(user._id, { max_side: 256 }, true)}
previewURL={ctx.client.users.getAvatarURL(user._id, { max_side: 256 }, true, true)}
onUpload={(avatar) => client.users.edit({ avatar })}
remove={() => client.users.edit({ remove: "Avatar" })}
defaultPreview={client.user!.generateAvatarURL(
{ max_side: 256 },
true,
)}
previewURL={client.user!.generateAvatarURL(
{ max_side: 256 },
true,
)}
/>
</div>
<div className={styles.background}>
......@@ -78,30 +104,43 @@ export function Profile() {
behaviour="upload"
fileType="backgrounds"
maxFileSize={6_000_000}
onUpload={async background => {
await ctx.client.users.editUser({ profile: { background } });
onUpload={async (background) => {
await client.users.edit({
profile: { background },
});
refreshProfile();
}}
remove={async () => {
await ctx.client.users.editUser({ remove: 'ProfileBackground' });
await client.users.edit({
remove: "ProfileBackground",
});
setProfile({ ...profile, background: undefined });
}}
previewURL={profile?.background ? ctx.client.users.getBackgroundURL(profile, { width: 1000 }, true) : undefined}
previewURL={
profile?.background
? client.generateFileURL(
profile.background,
{ width: 1000 },
true,
)
: undefined
}
/>
</div>
</div>
<h3>
<Text id="app.settings.pages.profile.info" />
</h3>
<AutoComplete detached {...autoCompleteProps} />
<TextAreaAutoSize
maxRows={10}
minHeight={200}
maxLength={2000}
value={profile?.content ?? ""}
disabled={typeof profile === "undefined"}
onChange={ev => {
setProfile({ ...profile, content: ev.currentTarget.value })
if (!changed) setChanged(true)
onChange={(ev) => {
onChange(ev);
setContent(ev.currentTarget.value);
}}
placeholder={translate(
`app.settings.pages.profile.${
......@@ -109,18 +148,25 @@ export function Profile() {
? "fetching"
: "placeholder"
}`,
"",
intl.dictionary
)}
onKeyUp={onKeyUp}
onKeyDown={onKeyDown}
onFocus={onFocus}
onBlur={onBlur}
/>
<Button contrast
onClick={() => {
setChanged(false);
ctx.client.users.editUser({ profile: { content: profile?.content } })
}}
disabled={!changed}>
<Text id="app.special.modals.actions.save" />
</Button>
<p>
<Button
contrast
onClick={() => {
setChanged(false);
client.users.edit({
profile: { content: profile?.content },
});
}}
disabled={!changed}>
<Text id="app.special.modals.actions.save" />
</Button>
</p>
</div>
);
}
import dayjs from "dayjs";
import { decodeTime } from "ulid";
import { Text } from "preact-i18n";
import styles from "./Panes.module.scss";
import Tip from "../../../components/ui/Tip";
import { useHistory } from "react-router-dom";
import Button from "../../../components/ui/Button";
import Preloader from "../../../components/ui/Preloader";
import { useContext, useEffect, useState } from "preact/hooks";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { HelpCircle } from "@styled-icons/feather";
import { Chrome, Android, Apple, Windows } from "@styled-icons/boxicons-logos";
import { HelpCircle, Desktop } from "@styled-icons/boxicons-regular";
import {
Android,
Safari,
Firefoxbrowser,
Googlechrome,
Ios,
Microsoftedge,
Linux,
Macos,
Microsoftedge,
Safari,
Windows
Opera,
} from "@styled-icons/simple-icons";
import relativeTime from "dayjs/plugin/relativeTime";
import { useHistory } from "react-router-dom";
import { decodeTime } from "ulid";
import styles from "./Panes.module.scss";
import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks";
import { dayjs } from "../../../context/Locale";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import Button from "../../../components/ui/Button";
import Preloader from "../../../components/ui/Preloader";
import Tip from "../../../components/ui/Tip";
dayjs.extend(relativeTime);
interface Session {
......@@ -43,14 +43,14 @@ export function Sessions() {
}
useEffect(() => {
client.req("GET", "/auth/sessions").then(data => {
client.req("GET", "/auth/sessions").then((data) => {
data.sort(
(a, b) =>
(b.id === deviceId ? 1 : 0) - (a.id === deviceId ? 1 : 0)
(b.id === deviceId ? 1 : 0) - (a.id === deviceId ? 1 : 0),
);
setSessions(data);
});
}, []);
}, [client, setSessions, deviceId]);
if (typeof sessions === "undefined") {
return (
......@@ -64,15 +64,19 @@ export function Sessions() {
const name = session.friendly_name;
switch (true) {
case /firefox/i.test(name):
return <Firefoxbrowser />;
return <Firefoxbrowser size={32} />;
case /chrome/i.test(name):
return <Googlechrome />;
return <Chrome size={32} />;
case /safari/i.test(name):
return <Safari />;
return <Safari size={32} />;
case /edge/i.test(name):
return <Microsoftedge />;
return <Microsoftedge size={32} />;
case /opera/i.test(name):
return <Opera size={32} />;
case /desktop/i.test(name):
return <Desktop size={32} />;
default:
return <HelpCircle />;
return <HelpCircle size={32} />;
}
}
......@@ -80,34 +84,34 @@ export function Sessions() {
const name = session.friendly_name;
switch (true) {
case /linux/i.test(name):
return <Linux />;
return <Linux size={14} />;
case /android/i.test(name):
return <Android />;
return <Android size={14} />;
case /mac.*os/i.test(name):
return <Macos />;
return <Macos size={14} />;
case /ios/i.test(name):
return <Ios />;
return <Apple size={14} />;
case /windows/i.test(name):
return <Windows />;
return <Windows size={14} />;
default:
return null;
}
}
const mapped = sessions.map(session => {
const mapped = sessions.map((session) => {
return {
...session,
timestamp: decodeTime(session.id)
timestamp: decodeTime(session.id),
};
});
mapped.sort((a, b) => b.timestamp - a.timestamp);
let id = mapped.findIndex(x => x.id === deviceId);
const id = mapped.findIndex((x) => x.id === deviceId);
const render = [
mapped[id],
...mapped.slice(0, id),
...mapped.slice(id + 1, mapped.length)
...mapped.slice(id + 1, mapped.length),
];
return (
......@@ -115,64 +119,116 @@ export function Sessions() {
<h3>
<Text id="app.settings.pages.sessions.active_sessions" />
</h3>
{render.map(session => (
<div
className={styles.entry}
data-active={session.id === deviceId}
data-deleting={attemptingDelete.indexOf(session.id) > -1}
>
{deviceId === session.id && (
<span className={styles.label}>
<Text id="app.settings.pages.sessions.this_device" />{" "}
</span>
)}
<div className={styles.session}>
<div className={styles.icon}>
{getIcon(session)}
<div>{getSystemIcon(session)}</div>
</div>
<div className={styles.info}>
<span className={styles.name}>
{session.friendly_name}
{render.map((session) => {
const systemIcon = getSystemIcon(session);
return (
<div
key={session.id}
className={styles.entry}
data-active={session.id === deviceId}
data-deleting={
attemptingDelete.indexOf(session.id) > -1
}>
{deviceId === session.id && (
<span className={styles.label}>
<Text id="app.settings.pages.sessions.this_device" />{" "}
</span>
<span className={styles.time}>
<Text
id="app.settings.pages.sessions.created"
fields={{
time_ago: dayjs(
session.timestamp
).fromNow()
)}
<div className={styles.session}>
<div className={styles.detail}>
<svg width={42} height={42} viewBox="0 0 32 32">
<foreignObject
x="0"
y="0"
width="32"
height="32"
mask={
systemIcon
? "url(#session)"
: undefined
}>
{getIcon(session)}
</foreignObject>
<foreignObject
x="18"
y="18"
width="14"
height="14">
{systemIcon}
</foreignObject>
</svg>
<div className={styles.info}>
<input
type="text"
className={styles.name}
value={session.friendly_name}
autocomplete="off"
style={{ pointerEvents: "none" }}
/>
<span className={styles.time}>
<Text
id="app.settings.pages.sessions.created"
fields={{
time_ago: dayjs(
session.timestamp,
).fromNow(),
}}
/>
</span>
</div>
</div>
{deviceId !== session.id && (
<Button
onClick={async () => {
setDelete([
...attemptingDelete,
session.id,
]);
await client.req(
"DELETE",
`/auth/sessions/${session.id}` as "/auth/sessions",
);
setSessions(
sessions?.filter(
(x) => x.id !== session.id,
),
);
}}
/>
</span>
disabled={
attemptingDelete.indexOf(session.id) >
-1
}>
<Text id="app.settings.pages.logOut" />
</Button>
)}
</div>
{deviceId !== session.id && (
<Button
onClick={async () => {
setDelete([
...attemptingDelete,
session.id
]);
await client.req(
"DELETE",
`/auth/sessions/${session.id}` as any
);
setSessions(
sessions?.filter(
x => x.id !== session.id
)
);
}}
disabled={
attemptingDelete.indexOf(session.id) > -1
}
>
<Text id="app.settings.pages.logOut" />
</Button>
)}
</div>
</div>
))}
);
})}
<Button
error
onClick={async () => {
// ! FIXME: add to rAuth
const del: string[] = [];
render.forEach((session) => {
if (deviceId !== session.id) {
del.push(session.id);
}
});
setDelete(del);
for (const id of del) {
await client.req(
"DELETE",
`/auth/sessions/${id}` as "/auth/sessions",
);
}
setSessions(sessions.filter((x) => x.id === deviceId));
}}>
<Text id="app.settings.pages.sessions.logout" />
</Button>
<Tip>
<span>
<Text id="app.settings.tips.sessions.a" />
......
import { Text } from "preact-i18n";
import styles from "./Panes.module.scss";
import Checkbox from "../../../components/ui/Checkbox";
import { Text } from "preact-i18n";
import { dispatch } from "../../../redux";
import { connectState } from "../../../redux/connector";
import { WithDispatcher } from "../../../redux/reducers";
import { SyncKeys, SyncOptions } from "../../../redux/reducers/sync";
import Checkbox from "../../../components/ui/Checkbox";
interface Props {
options?: SyncOptions;
}
export function Component(props: Props & WithDispatcher) {
export function Component(props: Props) {
return (
<div className={styles.notifications}>
<h3>
<Text id="app.settings.pages.sync.categories" />
</h3>
{
([
['appearance', 'appearance.title'],
['theme', 'appearance.theme'],
['locale', 'language.title']
] as [ SyncKeys, string ][]).map(
([ key, title ]) =>
<Checkbox
checked={(props.options?.disabled ?? []).indexOf(key) === -1}
onChange={enabled => {
props.dispatcher({
type: enabled ? 'SYNC_ENABLE_KEY' : 'SYNC_DISABLE_KEY',
key
});
}}
>
<Text id={`app.settings.pages.${title}`} />
<p>
<Text id={`app.settings.pages.sync.descriptions.${key}`} />
</p>
</Checkbox>
)
}
{(
[
["appearance", "appearance.title"],
["theme", "appearance.theme"],
["locale", "language.title"],
// notifications sync is always-on
] as [SyncKeys, string][]
).map(([key, title]) => (
<Checkbox
key={key}
checked={
(props.options?.disabled ?? []).indexOf(key) === -1
}
description={
<Text
id={`app.settings.pages.sync.descriptions.${key}`}
/>
}
onChange={(enabled) =>
dispatch({
type: enabled
? "SYNC_ENABLE_KEY"
: "SYNC_DISABLE_KEY",
key,
})
}>
<Text id={`app.settings.pages.${title}`} />
</Checkbox>
))}
</div>
);
}
export const Sync = connectState(
Component,
state => {
return {
options: state.sync
};
},
true
);
export const Sync = connectState(Component, (state) => {
return {
options: state.sync,
};
});
import { Servers } from "revolt.js/dist/api/objects";
import { useContext, useEffect, useState } from "preact/hooks";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { XCircle } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite";
import { Route } from "revolt.js/dist/api/routes";
import { Server } from "revolt.js/dist/maps/Servers";
import styles from "./Panes.module.scss";
import { Text } from "preact-i18n";
import { useEffect, useState } from "preact/hooks";
import UserIcon from "../../../components/common/user/UserIcon";
import IconButton from "../../../components/ui/IconButton";
import Preloader from "../../../components/ui/Preloader";
interface Props {
server: Servers.Server;
server: Server;
}
export function Bans({ server }: Props) {
const client = useContext(AppContext);
const [bans, setBans] = useState<Servers.Ban[] | undefined>(undefined);
export const Bans = observer(({ server }: Props) => {
const [deleting, setDelete] = useState<string[]>([]);
const [data, setData] = useState<
Route<"GET", "/servers/id/bans">["response"] | undefined
>(undefined);
useEffect(() => {
client.servers.fetchBans(server._id)
.then(bans => setBans(bans))
}, [ ]);
server.fetchBans().then(setData);
}, [server, setData]);
return (
<div>
{ bans?.map(x => <div>{x._id.user}: {x.reason ?? 'no reason'} <button onClick={() => client.servers.unbanUser(server._id, x._id.user)}>unban</button></div>) }
<div className={styles.userList}>
<div className={styles.subtitle}>
<span>
<Text id="app.settings.server_pages.bans.user" />
</span>
<span class={styles.reason}>
<Text id="app.settings.server_pages.bans.reason" />
</span>
<span>
<Text id="app.settings.server_pages.bans.revoke" />
</span>
</div>
{typeof data === "undefined" && <Preloader type="ring" />}
{data?.bans.map((x) => {
const user = data.users.find((y) => y._id === x._id.user);
return (
<div
key={x._id.user}
className={styles.ban}
data-deleting={deleting.indexOf(x._id.user) > -1}>
<span>
<UserIcon attachment={user?.avatar} size={24} />
{user?.username}
</span>
<div className={styles.reason}>
{x.reason ?? (
<Text id="app.settings.server_pages.bans.no_reason" />
)}
</div>
<IconButton
onClick={async () => {
setDelete([...deleting, x._id.user]);
await server.unbanUser(x._id.user);
setData({
...data,
bans: data.bans.filter(
(y) => y._id.user !== x._id.user,
),
});
}}
disabled={deleting.indexOf(x._id.user) > -1}>
<XCircle size={24} />
</IconButton>
</div>
);
})}
</div>
);
}
});
import isEqual from "lodash.isequal";
import { observer } from "mobx-react-lite";
import { Category } from "revolt-api/types/Servers";
import { Server } from "revolt.js/dist/maps/Servers";
import { ulid } from "ulid";
import { useState } from "preact/hooks";
import ChannelIcon from "../../../components/common/ChannelIcon";
import Button from "../../../components/ui/Button";
import ComboBox from "../../../components/ui/ComboBox";
import InputBox from "../../../components/ui/InputBox";
import Tip from "../../../components/ui/Tip";
interface Props {
server: Server;
}
// ! FIXME: really bad code
export const Categories = observer(({ server }: Props) => {
const channels = server.channels.filter((x) => typeof x !== "undefined");
const [cats, setCats] = useState<Category[]>(server.categories ?? []);
const [name, setName] = useState("");
return (
<div>
<Tip warning>This section is under construction.</Tip>
<p>
<Button
contrast
disabled={isEqual(server.categories ?? [], cats)}
onClick={() => server.edit({ categories: cats })}>
save categories
</Button>
</p>
<h2>categories</h2>
{cats.map((category) => (
<div style={{ background: "var(--hover)" }} key={category.id}>
<InputBox
value={category.title}
onChange={(e) =>
setCats(
cats.map((y) =>
y.id === category.id
? {
...y,
title: e.currentTarget.value,
}
: y,
),
)
}
contrast
/>
<Button
contrast
onClick={() =>
setCats(cats.filter((x) => x.id !== category.id))
}>
delete {category.title}
</Button>
</div>
))}
<h2>create new</h2>
<p>
<InputBox
value={name}
onChange={(e) => setName(e.currentTarget.value)}
contrast
/>
<Button
contrast
onClick={() => {
setName("");
setCats([
...cats,
{
id: ulid(),
title: name,
channels: [],
},
]);
}}>
create
</Button>
</p>
<h2>channels</h2>
{channels.map((channel) => {
return (
<div
key={channel!._id}
style={{
display: "flex",
gap: "12px",
alignItems: "center",
}}>
<div style={{ flexShrink: 0 }}>
<ChannelIcon target={channel} size={24} />{" "}
<span>{channel!.name}</span>
</div>
<ComboBox
style={{ flexGrow: 1 }}
value={
cats.find((x) =>
x.channels.includes(channel!._id),
)?.id ?? "none"
}
onChange={(e) =>
setCats(
cats.map((x) => {
return {
...x,
channels: [
...x.channels.filter(
(y) => y !== channel!._id,
),
...(e.currentTarget.value ===
x.id
? [channel!._id]
: []),
],
};
}),
)
}>
<option value="none">Uncategorised</option>
{cats.map((x) => (
<option key={x.id} value={x.id}>
{x.title}
</option>
))}
</ComboBox>
</div>
);
})}
</div>
);
});
import styles from './Panes.module.scss';
import { XCircle } from "@styled-icons/feather";
import { XCircle } from "@styled-icons/boxicons-regular";
import { observer } from "mobx-react-lite";
import { ServerInvite } from "revolt-api/types/Invites";
import { Server } from "revolt.js/dist/maps/Servers";
import styles from "./Panes.module.scss";
import { Text } from "preact-i18n";
import { useEffect, useState } from "preact/hooks";
import Preloader from "../../../components/ui/Preloader";
import IconButton from "../../../components/ui/IconButton";
import UserIcon from "../../../components/common/user/UserIcon";
import { useClient } from "../../../context/revoltjs/RevoltClient";
import { getChannelName } from "../../../context/revoltjs/util";
import { Invites as InvitesNS, Servers } from "revolt.js/dist/api/objects";
import { useChannels, useForceUpdate, useUsers } from "../../../context/revoltjs/hooks";
import UserIcon from "../../../components/common/user/UserIcon";
import IconButton from "../../../components/ui/IconButton";
import Preloader from "../../../components/ui/Preloader";
interface Props {
server: Servers.Server;
server: Server;
}
export function Invites({ server }: Props) {
const [invites, setInvites] = useState<InvitesNS.ServerInvite[] | undefined>(undefined);
const ctx = useForceUpdate();
export const Invites = observer(({ server }: Props) => {
const [deleting, setDelete] = useState<string[]>([]);
const users = useUsers(invites?.map(x => x.creator) ?? [], ctx);
const channels = useChannels(invites?.map(x => x.channel) ?? [], ctx);
const [invites, setInvites] = useState<ServerInvite[] | undefined>(
undefined,
);
const client = useClient();
const users = invites?.map((invite) => client.users.get(invite.creator));
const channels = invites?.map((invite) =>
client.channels.get(invite.channel),
);
useEffect(() => {
ctx.client.servers.fetchInvites(server._id)
.then(invites => setInvites(invites))
}, [ ]);
server.fetchInvites().then(setInvites);
}, [server, setInvites]);
return (
<div className={styles.invites}>
{ typeof invites === 'undefined' && <Preloader type="ring" /> }
{
invites?.map(
invite => {
let creator = users.find(x => x?._id === invite.creator);
let channel = channels.find(x => x?._id === invite.channel);
<div className={styles.userList}>
<div className={styles.subtitle}>
<span>
<Text id="app.settings.server_pages.invites.code" />
</span>
<span>
<Text id="app.settings.server_pages.invites.invitor" />
</span>
<span>
<Text id="app.settings.server_pages.invites.channel" />
</span>
<span>
<Text id="app.settings.server_pages.invites.revoke" />
</span>
</div>
{typeof invites === "undefined" && <Preloader type="ring" />}
{invites?.map((invite, index) => {
const creator = users![index];
const channel = channels![index];
return (
<div
key={invite._id}
className={styles.invite}
data-deleting={deleting.indexOf(invite._id) > -1}>
<code>{invite._id}</code>
<span>
<UserIcon target={creator} size={24} />{" "}
{creator?.username ?? (
<Text id="app.main.channel.unknown_user" />
)}
</span>
<span>
{channel && creator
? getChannelName(channel, true)
: "#??"}
</span>
<IconButton
onClick={async () => {
setDelete([...deleting, invite._id]);
return (
<div className={styles.invite}
data-deleting={deleting.indexOf(invite._id) > -1}>
<code>{ invite._id }</code>
<span>
<UserIcon target={creator} size={24} /> {creator?.username ?? 'unknown'}
</span>
<span>{ (channel && creator) ? getChannelName(ctx.client, channel, true) : '#unknown' }</span>
<IconButton
onClick={async () => {
setDelete([
...deleting,
invite._id
]);
await client.deleteInvite(invite._id);
await ctx.client.deleteInvite(invite._id);
setInvites(
invites?.filter(
x => x._id !== invite._id
)
);
}}
disabled={deleting.indexOf(invite._id) > -1}>
<XCircle size={24} />
</IconButton>
</div>
)
}
)
}
setInvites(
invites?.filter(
(x) => x._id !== invite._id,
),
);
}}
disabled={deleting.indexOf(invite._id) > -1}>
<XCircle size={24} />
</IconButton>
</div>
);
})}
</div>
);
}
});
import { ChevronDown } from "@styled-icons/boxicons-regular";
import { isEqual } from "lodash";
import { observer } from "mobx-react-lite";
import { Member } from "revolt.js/dist/maps/Members";
import { Server } from "revolt.js/dist/maps/Servers";
import { User } from "revolt.js/dist/maps/Users";
import styles from "./Panes.module.scss";
import { Text } from "preact-i18n";
import { useEffect, useState } from "preact/hooks";
import { Servers } from "revolt.js/dist/api/objects";
import { useForceUpdate, useUsers } from "../../../context/revoltjs/hooks";
import UserIcon from "../../../components/common/user/UserIcon";
import Button from "../../../components/ui/Button";
import Checkbox from "../../../components/ui/Checkbox";
import IconButton from "../../../components/ui/IconButton";
import Overline from "../../../components/ui/Overline";
interface Props {
server: Servers.Server;
server: Server;
}
export function Members({ server }: Props) {
const [members, setMembers] = useState<Servers.Member[] | undefined>(undefined);
const ctx = useForceUpdate();
const users = useUsers(members?.map(x => x._id.user) ?? [], ctx);
export const Members = observer(({ server }: Props) => {
const [selected, setSelected] = useState<undefined | string>();
const [data, setData] = useState<
{ members: Member[]; users: User[] } | undefined
>(undefined);
useEffect(() => {
ctx.client.servers.members.fetchMembers(server._id)
.then(members => setMembers(members))
}, [ ]);
server.fetchMembers().then(setData);
}, [server, setData]);
const [roles, setRoles] = useState<string[]>([]);
useEffect(() => {
if (selected) {
setRoles(
data!.members.find((x) => x._id.user === selected)?.roles ?? [],
);
}
}, [setRoles, selected, data]);
return (
<div>
{ members && members.length > 0 && users?.map(x => x && <div>@{x.username}</div>) }
<div className={styles.userList}>
<div className={styles.subtitle}>
{data?.members.length ?? 0} Members
</div>
{data &&
data.members.length > 0 &&
data.members
.map((member) => {
return {
member,
user: data.users.find(
(x) => x._id === member._id.user,
),
};
})
.map(({ member, user }) => (
// @ts-expect-error brokey
// eslint-disable-next-line react/jsx-no-undef
<Fragment key={member._id.user}>
<div
className={styles.member}
data-open={selected === member._id.user}
onClick={() =>
setSelected(
selected === member._id.user
? undefined
: member._id.user,
)
}>
<span>
<UserIcon target={user} size={24} />{" "}
{user?.username ?? (
<Text id="app.main.channel.unknown_user" />
)}
</span>
<IconButton className={styles.chevron}>
<ChevronDown size={24} />
</IconButton>
</div>
{selected === member._id.user && (
<div
key={`drop_${member._id.user}`}
className={styles.memberView}>
<Overline type="subtle">Roles</Overline>
{Object.keys(server.roles ?? {}).map(
(key) => {
const role = server.roles![key];
return (
<Checkbox
key={key}
checked={
roles.includes(key) ??
false
}
onChange={(v) => {
if (v) {
setRoles([
...roles,
key,
]);
} else {
setRoles(
roles.filter(
(x) =>
x !==
key,
),
);
}
}}>
<span
style={{
color: role.colour,
}}>
{role.name}
</span>
</Checkbox>
);
},
)}
<Button
compact
disabled={isEqual(
member.roles ?? [],
roles,
)}
onClick={() =>
member.edit({
roles,
})
}>
<Text id="app.special.modals.actions.save" />
</Button>
</div>
)}
</Fragment>
))}
</div>
);
}
});
import isEqual from "lodash.isequal";
import { observer } from "mobx-react-lite";
import { Server } from "revolt.js/dist/maps/Servers";
import styles from "./Panes.module.scss";
import { Text } from "preact-i18n";
import styles from './Panes.module.scss';
import Button from "../../../components/ui/Button";
import { Servers } from "revolt.js/dist/api/objects";
import InputBox from "../../../components/ui/InputBox";
import { useEffect, useState } from "preact/hooks";
import TextAreaAutoSize from "../../../lib/TextAreaAutoSize";
import { useContext, useEffect, useState } from "preact/hooks";
import { AppContext } from "../../../context/revoltjs/RevoltClient";
import { FileUploader } from "../../../context/revoltjs/FileUploads";
import { getChannelName } from "../../../context/revoltjs/util";
import Button from "../../../components/ui/Button";
import ComboBox from "../../../components/ui/ComboBox";
import InputBox from "../../../components/ui/InputBox";
interface Props {
server: Servers.Server;
server: Server;
}
export function Overview({ server }: Props) {
const client = useContext(AppContext);
export const Overview = observer(({ server }: Props) => {
const [name, setName] = useState(server.name);
const [description, setDescription] = useState(server.description ?? '');
const [description, setDescription] = useState(server.description ?? "");
const [systemMessages, setSystemMessages] = useState(
server.system_messages,
);
useEffect(() => setName(server.name), [ server.name ]);
useEffect(() => setDescription(server.description ?? ''), [ server.description ]);
useEffect(() => setName(server.name), [server.name]);
useEffect(
() => setDescription(server.description ?? ""),
[server.description],
);
useEffect(
() => setSystemMessages(server.system_messages),
[server.system_messages],
);
const [ changed, setChanged ] = useState(false);
const [changed, setChanged] = useState(false);
function save() {
let changes: any = {};
const changes: Record<string, unknown> = {};
if (name !== server.name) changes.name = name;
if (description !== server.description)
changes.description = description;
client.servers.edit(server._id, changes);
if (!isEqual(systemMessages, server.system_messages))
changes.system_messages = systemMessages ?? undefined;
server.edit(changes);
setChanged(false);
}
......@@ -42,9 +59,9 @@ export function Overview({ server }: Props) {
fileType="icons"
behaviour="upload"
maxFileSize={2_500_000}
onUpload={icon => client.servers.edit(server._id, { icon })}
previewURL={client.servers.getIconURL(server._id, { max_side: 256 }, true)}
remove={() => client.servers.edit(server._id, { remove: 'Icon' })}
onUpload={(icon) => server.edit({ icon })}
previewURL={server.generateIconURL({ max_side: 256 }, true)}
remove={() => server.edit({ remove: "Icon" })}
/>
<div className={styles.name}>
<h3>
......@@ -54,9 +71,9 @@ export function Overview({ server }: Props) {
contrast
value={name}
maxLength={32}
onChange={e => {
setName(e.currentTarget.value)
if (!changed) setChanged(true)
onChange={(e) => {
setName(e.currentTarget.value);
if (!changed) setChanged(true);
}}
/>
</div>
......@@ -71,14 +88,11 @@ export function Overview({ server }: Props) {
maxLength={1024}
value={description}
placeholder={"Add a topic..."}
onChange={ev => {
onChange={(ev) => {
setDescription(ev.currentTarget.value);
if (!changed) setChanged(true)
if (!changed) setChanged(true);
}}
/>
<Button onClick={save} contrast disabled={!changed}>
<Text id="app.special.modals.actions.save" />
</Button>
<h3>
<Text id="app.main.servers.custom_banner" />
......@@ -89,10 +103,70 @@ export function Overview({ server }: Props) {
fileType="banners"
behaviour="upload"
maxFileSize={6_000_000}
onUpload={banner => client.servers.edit(server._id, { banner })}
previewURL={client.servers.getBannerURL(server._id, { width: 1000 }, true)}
remove={() => client.servers.edit(server._id, { remove: 'Banner' })}
onUpload={(banner) => server.edit({ banner })}
previewURL={server.generateBannerURL({ width: 1000 }, true)}
remove={() => server.edit({ remove: "Banner" })}
/>
<h3>
<Text id="app.settings.server_pages.overview.system_messages" />
</h3>
{[
["User Joined", "user_joined"],
["User Left", "user_left"],
["User Kicked", "user_kicked"],
["User Banned", "user_banned"],
].map(([i18n, key]) => (
// ! FIXME: temporary code just so we can expose the options
<p
key={key}
style={{
display: "flex",
gap: "8px",
alignItems: "center",
}}>
<span style={{ flexShrink: "0", flex: `25%` }}>{i18n}</span>
<ComboBox
value={
systemMessages?.[
key as keyof typeof systemMessages
] ?? "disabled"
}
onChange={(e) => {
if (!changed) setChanged(true);
const v = e.currentTarget.value;
if (v === "disabled") {
const {
[key as keyof typeof systemMessages]: _,
...other
} = systemMessages;
setSystemMessages(other);
} else {
setSystemMessages({
...systemMessages,
[key]: v,
});
}
}}>
<option value="disabled">
<Text id="general.disabled" />
</option>
{server.channels
.filter((x) => typeof x !== "undefined")
.map((channel) => (
<option key={channel!._id} value={channel!._id}>
{getChannelName(channel!, true)}
</option>
))}
</ComboBox>
</p>
))}
<p>
<Button onClick={save} contrast disabled={!changed}>
<Text id="app.special.modals.actions.save" />
</Button>
</p>
</div>
);
}
});