style the join (claim invite) pages

This commit is contained in:
Andre Staltz 2021-03-30 18:04:07 +03:00
parent 371109c136
commit 08c8b0cced
No known key found for this signature in database
GPG Key ID: 9EDE23EA7E8A4890
13 changed files with 226 additions and 74 deletions

BIN
docs/invites-chart.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

9
docs/invites.md Normal file
View File

@ -0,0 +1,9 @@
# Invites
This implementation of Rooms 2.0 is compliant with the [Rooms 2.0 specification](https://github.com/ssb-ngi-pointer/rooms2), but we add a few additional features and pages in order to improve user experience when their SSB app does not support [SSB URIs](https://github.com/ssb-ngi-pointer/ssb-uri-spec).
A summary can be seen in the following chart:
![Chart](./invites-chart.png)
When the browser and operating system detects no support for opening SSB URIs, we redirect to a fallback page which presents the user with two broad options: (1) install an SSB app that supports SSB URIs, (2) link to another page where the user can manually input the user's SSB ID in a form.

View File

@ -0,0 +1,17 @@
let hasFocus = true;
window.addEventListener('blur', () => {
hasFocus = false;
});
window.addEventListener('focus', () => {
hasFocus = true;
});
const anchorElem = document.getElementById('join-room-uri');
anchorElem.onclick = function handleURI(ev) {
const ssbUri = ev.target.dataset.href;
const fallbackUrl = ev.target.dataset.hrefFallback;
setTimeout(function () {
if (hasFocus) window.location = fallbackUrl;
}, 500);
window.location = ssbUri;
};

View File

@ -39,6 +39,8 @@ var HTMLTemplates = []string{
"invite/consumed.tmpl",
"invite/facade.tmpl",
"invite/facade-fallback.tmpl",
"invite/insert-id.tmpl",
"notice/list.tmpl",
"notice/show.tmpl",
@ -302,10 +304,13 @@ func New(
render: r,
invites: dbs.Invites,
pinnedNotices: dbs.PinnedNotices,
networkInfo: netInfo,
}
m.Get(router.CompleteInviteFacade).Handler(r.HTML("invite/facade.tmpl", ih.presentFacade))
m.Get(router.CompleteInviteFacadeFallback).Handler(r.HTML("invite/facade-fallback.tmpl", ih.presentFacadeFallback))
m.Get(router.CompleteInviteInsertID).Handler(r.HTML("invite/insert-id.tmpl", ih.presentInsert))
m.Get(router.CompleteInviteConsume).HandlerFunc(ih.consume)
m.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(web.Assets)))

View File

@ -1,15 +1,18 @@
package handlers
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"html/template"
"image/color"
"net/http"
"net/url"
"github.com/go-kit/kit/log/level"
"github.com/gorilla/csrf"
"github.com/skip2/go-qrcode"
"go.mindeco.de/http/render"
"go.mindeco.de/logging"
@ -25,21 +28,12 @@ type inviteHandler struct {
render *render.Renderer
invites roomdb.InvitesService
pinnedNotices roomdb.PinnedNoticesService
networkInfo network.ServerEndpointDetails
}
func (h inviteHandler) presentFacade(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
token := req.URL.Query().Get("token")
inv, err := h.invites.GetByToken(req.Context(), token)
if err != nil {
if errors.Is(err, roomdb.ErrNotFound) {
return nil, weberrors.ErrNotFound{What: "invite"}
}
return nil, err
}
func buildJoinRoomURI(h inviteHandler, token string) string {
var joinRoomURI url.URL
joinRoomURI.Scheme = "ssb"
joinRoomURI.Opaque = "experimental"
@ -60,13 +54,88 @@ func (h inviteHandler) presentFacade(rw http.ResponseWriter, req *http.Request)
joinRoomURI.RawQuery = queryVals.Encode()
return joinRoomURI.String()
}
func (h inviteHandler) presentFacade(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
token := req.URL.Query().Get("token")
_, err := h.invites.GetByToken(req.Context(), token)
if err != nil {
if errors.Is(err, roomdb.ErrNotFound) {
return nil, weberrors.ErrNotFound{What: "invite"}
}
return nil, err
}
notice, err := h.pinnedNotices.Get(req.Context(), roomdb.NoticeDescription, "en-GB")
if err != nil {
return nil, fmt.Errorf("failed to find room's description: %w", err)
}
joinRoomURI := buildJoinRoomURI(h, token)
urlTo := web.NewURLTo(router.CompleteApp())
fallbackURL := urlTo(router.CompleteInviteFacadeFallback, "token", token)
// generate a QR code with the token inside so that you can open it easily in a supporting mobile app
qrCode, err := qrcode.New(joinRoomURI, qrcode.Medium)
if err != nil {
return nil, err
}
qrCode.BackgroundColor = color.Transparent // transparent to fit into the page
qrCode.ForegroundColor = color.Black
qrCodeData, err := qrCode.PNG(-5)
if err != nil {
return nil, err
}
qrURI := "data:image/png;base64," + base64.StdEncoding.EncodeToString(qrCodeData)
return map[string]interface{}{
csrf.TemplateTag: csrf.TemplateField(req),
"RoomTitle": notice.Title,
"JoinRoomURI": template.URL(joinRoomURI),
"FallbackURL": fallbackURL,
"QRCodeURI": template.URL(qrURI),
}, nil
}
"Invite": inv,
func (h inviteHandler) presentFacadeFallback(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
token := req.URL.Query().Get("token")
_, err := h.invites.GetByToken(req.Context(), token)
if err != nil {
if errors.Is(err, roomdb.ErrNotFound) {
return nil, weberrors.ErrNotFound{What: "invite"}
}
return nil, err
}
urlTo := web.NewURLTo(router.CompleteApp())
insertURL := urlTo(router.CompleteInviteInsertID, "token", token)
return map[string]interface{}{
csrf.TemplateTag: csrf.TemplateField(req),
"InsertURL": insertURL,
}, nil
}
func (h inviteHandler) presentInsert(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
token := req.URL.Query().Get("token")
_, err := h.invites.GetByToken(req.Context(), token)
if err != nil {
if errors.Is(err, roomdb.ErrNotFound) {
return nil, weberrors.ErrNotFound{What: "invite"}
}
return nil, err
}
return map[string]interface{}{
csrf.TemplateTag: csrf.TemplateField(req),
"Token": token,
"JoinRoomURI": template.URL(joinRoomURI.String()),
}, nil
}
@ -213,22 +282,9 @@ func (html *inviteConsumeHTMLResponder) UpdateMultiserverAddr(msaddr string) {
}
func (html inviteConsumeHTMLResponder) SendSuccess() {
// construct the ssb:experimental?action=consume-invite&... uri for linking into apps
queryParams := url.Values{}
queryParams.Set("action", "join-room")
queryParams.Set("multiserverAddress", html.multiservAddr)
// html.multiservAddr
ssbURI := url.URL{
Scheme: "ssb",
Opaque: "experimental",
RawQuery: queryParams.Encode(),
}
err := html.renderer.Render(html.rw, html.req, "invite/consumed.tmpl", http.StatusOK, struct {
SSBURI template.URL
}{template.URL(ssbURI.String())})
MultiserverAddress string
}{(html.multiservAddr)})
if err != nil {
logger := logging.FromContext(html.req.Context())
level.Warn(logger).Log("event", "render failed", "err", err)

View File

@ -2,12 +2,15 @@ GenericConfirm = "Yes"
GenericGoBack = "Back"
GenericSave = "Save"
GenericCreate = "Create"
GenericSubmit = "Submit"
GenericPreview = "Preview"
GenericLanguage = "Language"
GenericOpenLink = "Open Link"
PageNotFound = "The requested page was not found."
PubKeyRefPlaceholder = "@ .ed25519"
RoleMember = "Member"
RoleModerator = "Moderator"
RoleAdmin = "Admin"
@ -82,12 +85,18 @@ NavAdminNotices = "Notices"
InviteFacade = "Join Room"
InviteFacadeTitle = "Join Room"
InviteFacadeWelcome = "elaborate welcome message for a new member with good words and stuff."
InviteFacadeAliasSuggestion = "The persone who created thought you might like this alias:"
InviteFacadePublicKey = "Public Key"
InviteFacadeWelcome = "You have permission to become a member of this room because someone has shared this invite with you."
InviteFacadeInstruct = "To claim the invite, press the button below which will open a compatible SSB app, if it's installed."
InviteFacadeJoin = "Join this room"
InviteFacadeFallbackWelcome = "Are you new to SSB? It seems you don't have an SSB app capable of understanding that link. You can install one of these apps:"
InviteFacadeFallbackManyverse = "Install Manyverse"
InviteFacadeFallbackInsertID = "Insert SSB ID"
InviteInsertWelcome = "You can claim your invite by inserting your SSB ID below. After that, you'll be able to connect to the room in your SSB app."
InviteConsumedTitle = "Invite accepted!"
InviteConsumedWelcome = "Even more elaborate message that the person is now a member of the room!"
InviteConsumedWelcome = "You are now a member of this room. If you need a multiserver address to connect to the room, you can copy-paste the one below:"
NoticeEditTitle = "Edit Notice"
NoticeList = "Notices"

View File

@ -17,6 +17,8 @@ const (
CompleteAliasResolve = "complete:alias:resolve"
CompleteInviteFacade = "complete:invite:accept"
CompleteInviteFacadeFallback = "complete:invite:accept:fallback"
CompleteInviteInsertID = "complete:invite:insert-id"
CompleteInviteConsume = "complete:invite:consume"
)
@ -33,6 +35,8 @@ func CompleteApp() *mux.Router {
m.Path("/alias/{alias}").Methods("GET").Name(CompleteAliasResolve)
m.Path("/join").Methods("GET").Name(CompleteInviteFacade)
m.Path("/join-fallback").Methods("GET").Name(CompleteInviteFacadeFallback)
m.Path("/join-manually").Methods("GET").Name(CompleteInviteInsertID)
m.Path("/invite/consume").Methods("POST").Name(CompleteInviteConsume)
m.Path("/notice/show").Methods("GET").Name(CompleteNoticeShow)

View File

@ -22,7 +22,7 @@
<input
type="text"
name="pub_key"
placeholder="@ .ed25519"
placeholder="{{i18n "PubKeyRefPlaceholder"}}"
class="font-mono truncate w-1/2 mr-2 tracking-wider h-12 text-gray-900 focus:outline-none focus:ring-1 focus:ring-green-500 focus:border-transparent placeholder-gray-300"
>
<input

View File

@ -22,7 +22,7 @@
<input
type="text"
name="pub_key"
placeholder="@ .ed25519"
placeholder="{{i18n "PubKeyRefPlaceholder"}}"
class="font-mono truncate w-1/2 ml-3 tracking-wider h-12 text-gray-900 focus:outline-none focus:ring-1 focus:ring-green-500 focus:border-transparent placeholder-gray-300"
>
<input

View File

@ -1,12 +1,17 @@
{{ define "title" }}{{i18n "InviteConsumedTitle"}}{{ end }}
{{ define "content" }}
<div class="flex flex-col justify-center items-center h-64">
<div class="flex flex-col justify-center items-center self-center max-w-lg">
<svg class="mt-6 w-32 h-32 text-green-300" viewBox="0 0 24 24">
<path fill="currentColor" d="M12 2C6.5 2 2 6.5 2 12S6.5 22 12 22 22 17.5 22 12 17.5 2 12 2M12 20C7.59 20 4 16.41 4 12S7.59 4 12 4 20 7.59 20 12 16.41 20 12 20M16.59 7.58L10 14.17L7.41 11.59L6 13L10 17L18 9L16.59 7.58Z" />
</svg>
<span
id="welcome"
class="text-center"
>{{i18n "InviteConsumedWelcome"}}</span>
class="my-6 text-center"
>{{i18n "InviteConsumedTitle"}}<br />{{i18n "InviteConsumedWelcome"}}</span>
<a id="join-link" href="{{.SSBURI}}">Join Room</a>
<span
class="bg-gray-200 py-1 px-2 mb-8 w-64 font-mono break-all"
>{{.MultiserverAddress}}</span>
</div>
{{end}}

View File

@ -0,0 +1,29 @@
{{ define "title" }}{{ i18n "InviteFacadeTitle" }}{{ end }}
{{ define "content" }}
<div class="flex flex-col justify-center items-center self-center max-w-lg">
<p class="text-center mt-8">{{i18n "InviteFacadeFallbackWelcome"}}</p>
<div class="flex flex-col sm:flex-row items-center mt-8">
<a
href="https://manyver.se"
class="shadow rounded flex flex-row justify-center items-center px-4 h-8 text-gray-100 bg-purple-500 hover:bg-purple-600 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:ring-opacity-50"
>{{i18n "InviteFacadeFallbackManyverse"}}</a>
<a
href="https://manyver.se"
class="mt-8 sm:ml-4 sm:mt-0 shadow rounded flex flex-row justify-center items-center px-4 h-8 text-gray-100 bg-purple-500 hover:bg-purple-600 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:ring-opacity-50"
>Install Imaginary</a>
</div>
<hr class="mt-8 w-64 h-px bg-gray-200"></hr>
<p
class="text-center my-8"
>Or if you already have an SSB app (such as <a class="text-pink-600 underline" href="https://github.com/ssbc/patchwork" target="_blank">Patchwork</a>, <a class="text-pink-600 underline" href="https://github.com/ssbc/patchbay" target="_blank">Patchbay</a>, <a class="text-pink-600 underline" href="http://git.scuttlebot.io/%25YAg1hicat%2B2GELjE2QJzDwlAWcx0ML%2B1sXEdsWwvdt8%3D.sha256" target="_blank">Patchfoo</a>, <a class="text-pink-600 underline" href="https://github.com/fraction/oasis" target="_blank">Oasis</a>, <a class="text-pink-600 underline" href="https://github.com/planetary-social/planetary-ios" target="_blank">Planetary</a>) and it couldn't process the link for whatever reason, you can manually input your identifier here:</p>
<a
href="{{.InsertURL}}"
class="mb-8 shadow rounded flex flex-row justify-center items-center px-4 h-8 text-gray-600 bg-white hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-600 focus:ring-opacity-50"
>{{i18n "InviteFacadeFallbackInsertID"}}</a>
</div>
{{ end }}

View File

@ -1,39 +1,30 @@
{{ define "title" }}{{ i18n "InviteFacadeTitle" }}{{ end }}
{{ define "content" }}
<div class="flex flex-col justify-center items-center h-64">
<span
id="welcome"
class="text-center"
>{{ i18n "InviteFacadeWelcome" }}</span>
<div class="flex flex-col justify-center items-center self-center max-w-lg">
<p id="welcome" class="text-center mt-8 italic">Welcome to {{.RoomTitle}}! {{i18n "InviteFacadeWelcome"}}</p>
<p class="text-center mt-3">{{i18n "InviteFacadeInstruct"}}</p>
<a
href="{{.JoinRoomURI}}"
class="my-8 shadow rounded px-4 h-8 text-gray-100 bg-green-500 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-600 focus:ring-opacity-50"
>Join</a>
id="join-room-uri"
href="#"
data-href="{{.JoinRoomURI}}"
data-href-fallback="{{.FallbackURL}}"
class="shadow rounded flex flex-row justify-center items-center mt-8 px-4 h-8 text-gray-100 bg-purple-500 hover:bg-purple-600 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:ring-opacity-50"
>{{i18n "InviteFacadeJoin"}}</a>
<hr class="mb-10 pt-10">
<h3 class="text-red-500">TODO: html form fallback / advanced use</h3>
<hr class="mt-8 w-64 h-px bg-gray-200"></hr>
<form id="consume" action="{{urlTo "complete:invite:consume"}}" method="POST">
{{ .csrfField }}
<input type="hidden" name="invite" value={{.Token}}>
<span class="text-center mt-8">{{i18n "AuthWithSSBInstructQR"}}</span>
<img
id="start-auth-qrcode"
src="{{.QRCodeURI}}"
alt="QR-Code to pass the challenge to an App"
width="160"
height="160"
class="mt-8"
/>
<div class="my-4 flex flex-row items-center justify-start">
<label class="mr-2">{{ i18n "InviteFacadePublicKey" }}</label>
<input
type="text"
name="id"
placeholder="@ .ed25519"
class="shadow rounded border border-transparent h-8 p-1 focus:outline-none focus:ring-2 focus:ring-pink-400 focus:border-transparent">
</div>
<button
type="submit"
class="shadow rounded px-4 h-8 text-gray-100 bg-pink-600 hover:bg-pink-700 focus:outline-none focus:ring-2 focus:ring-pink-600 focus:ring-opacity-50"
>{{i18n "GenericConfirm"}}</button>
</form>
</div>
<script src="/assets/autoredirect.js"></script>
</div>
{{ end }}

View File

@ -0,0 +1,27 @@
{{ define "title" }}{{ i18n "InviteFacadeTitle" }}{{ end }}
{{ define "content" }}
<div class="flex flex-col justify-center items-center self-center max-w-lg">
<span id="welcome" class="text-center mt-8">{{i18n "InviteInsertWelcome"}}</span>
<form
id="consume"
action="{{urlTo "complete:invite:consume"}}"
method="POST"
class="flex flex-col items-center self-stretch"
>
{{.csrfField}}
<input type="hidden" name="invite" value={{.Token}}>
<input
type="text"
name="id"
placeholder="{{i18n "PubKeyRefPlaceholder"}}"
class="mt-8 self-stretch shadow rounded border border-transparent h-10 p-1 pl-4 font-mono truncate flex-auto text-gray-600 focus:outline-none focus:ring-2 focus:ring-purple-400 focus:border-transparent">
<button
type="submit"
class="my-8 w-32 shadow rounded px-4 h-8 text-gray-100 bg-purple-500 hover:bg-purple-600 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:ring-opacity-50"
>{{i18n "GenericSubmit"}}</button>
</form>
</div>
{{ end }}