Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Embed editor state in URL; remove session storage #299

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Notable changes to this project are documented in this file. The format is based
Breaking changes:

New features:
- Remove `localStorage` for session storage, persist editor state in URL query param (#299 by @ptrfrncsmrph)

Bugfixes:

Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
- PureScript syntax highlighting
- Run and print output or show resulting JavaScript
- Multiple view modes: code, output or both
- Persistent session
- Shareable code and editor state via URL
- Load PureScript code from GitHub Gists or repository files

### Control Features via the Query String
Expand All @@ -28,6 +28,11 @@ Most of these features can be controlled not only from the toolbar, but also usi
- Example: `gist=37c3c97f47a43f20c548`
- Notes: the file should be named `Main.purs` with the module name `Main`.

- **Load From URL**: Load compressed PureScript code using the `code` parameter
- Managed by Try PureScript and updated on editor state change to create shareable URLs
- Format: `code=<compressed string>`
- Example: `code=LYewJgrgNgpgBAWQIYEsB2cDuALGAnGIA` will set the editor state to the single line `module Main where`

- **View Mode**: Control the view mode using the `view` parameter
- Options are: `code`, `output`, `both` (default)
- Example: `view=output` will only display the output
Expand All @@ -40,11 +45,6 @@ Most of these features can be controlled not only from the toolbar, but also usi
- Options are: `true`, `false` (default)
- Example: `js=true` will print JavaScript code instead of the program's output

- **Session**: Load code from a session which is stored with [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) using the `session` parameter
- Usually managed by Try PureScript
- Example: `session=9162f098-070f-4053-60ea-eba47021450d` (Note: will probably not work for you)
- When used with the `gist` or `github` query parameters the code will be loaded from the source file and not the session

### Which Libraries Are Available?

Try PureScript aims to provide a complete, recent package set from <https://github.com/purescript/package-sets>. The available libraries are those listed in [`staging/spago.dhall`](./staging/spago.dhall), at the versions in the package set mentioned in [`staging/packages.dhall`](./staging/packages.dhall).
Expand Down
3 changes: 2 additions & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
},
"dependencies": {
"ace-builds": "^1.5.0",
"jquery": "^1.12.4"
"jquery": "^1.12.4",
"lz-string": "^1.4.4"
}
}
11 changes: 11 additions & 0 deletions client/src/Try/Container.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,14 @@ export function setupIFrame(data, loadCb, failCb) {

return $iframe;
}

export function copyToClipboard(string, copyCb, failCb) {
try {
navigator.clipboard.writeText(string).then(
() => copyCb(),
() => failCb()
);
} catch (_error) {
failCb();
}
}
88 changes: 63 additions & 25 deletions client/src/Try/Container.purs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ import Ace (Annotation)
import Control.Monad.Except (runExceptT)
import Data.Array as Array
import Data.Either (Either(..), either)
import Data.Foldable (for_, oneOf, fold)
import Data.Foldable (for_, oneOf, fold, traverse_)
import Data.FoldableWithIndex (foldMapWithIndex)
import Data.Maybe (Maybe(..), fromMaybe, isNothing)
import Data.String as String
import Data.String (Pattern(..))
import Data.String.Regex as Regex
import Data.String.Regex.Flags as RegexFlags
import Effect (Effect)
import Effect.Aff (Aff, makeAff)
import Effect.Aff (Aff, Milliseconds(..), delay, makeAff)
import Effect.Aff as Aff
import Effect.Class.Console (error)
import Effect.Uncurried (EffectFn3, runEffectFn3)
Expand All @@ -30,14 +30,14 @@ import Try.Editor (MarkerType(..), toStringMarkerType)
import Try.Editor as Editor
import Try.Gist (getGistById, tryLoadFileFromGist)
import Try.GitHub (getRawGitHubFile)
import Try.QueryString (getQueryStringMaybe)
import Try.Session (createSessionIdIfNecessary, storeSession, tryRetrieveSession)
import Try.QueryString (compressToEncodedURIComponent, decompressFromEncodedURIComponent, getQueryStringMaybe, setQueryString)
import Try.SharedConfig as SharedConfig
import Type.Proxy (Proxy(..))
import Web.HTML (window)
import Web.HTML.Window (alert)
import Web.HTML.Location (href)
import Web.HTML.Window (alert, location)

type Slots = ( editor :: Editor.Slot Unit )
type Slots = ( editor :: Editor.Slot Unit, shareButton :: forall q o. H.Slot q o Unit )

data SourceFile = GitHub String | Gist String

Expand Down Expand Up @@ -76,18 +76,19 @@ parseViewModeParam = case _ of

data Action
= Initialize
| Cache String
| EncodeInURL String
| UpdateSettings (Settings -> Settings)
| Compile (Maybe String)
| HandleEditor Editor.Output

_editor :: Proxy "editor"
_editor = Proxy

type LoadCb = Effect Unit
type SucceedCb = Effect Unit
type FailCb = Effect Unit
foreign import setupIFrame :: EffectFn3 { code :: String } LoadCb FailCb Unit
foreign import setupIFrame :: EffectFn3 { code :: String } SucceedCb FailCb Unit
foreign import teardownIFrame :: Effect Unit
foreign import copyToClipboard :: EffectFn3 String SucceedCb FailCb Unit

component :: forall q i o. H.Component q i o Aff
component = H.mkComponent
Expand All @@ -109,8 +110,7 @@ component = H.mkComponent
handleAction :: Action -> H.HalogenM State Action Slots o Aff Unit
handleAction = case _ of
Initialize -> do
sessionId <- H.liftEffect $ createSessionIdIfNecessary
{ code, sourceFile } <- H.liftAff $ withSession sessionId
{ code, sourceFile } <- H.liftAff withSession

-- Load parameters
mbViewModeParam <- H.liftEffect $ getQueryStringMaybe "view"
Expand Down Expand Up @@ -140,13 +140,8 @@ component = H.mkComponent
else
handleAction $ Compile Nothing

Cache text -> H.liftEffect do
sessionId <- getQueryStringMaybe "session"
case sessionId of
Just sessionId_ -> do
storeSession sessionId_ { code: text }
Nothing ->
error "No session ID"
EncodeInURL text -> H.liftEffect do
setQueryString "code" $ compressToEncodedURIComponent text

Compile mbCode -> do
H.modify_ _ { compiled = Nothing }
Expand Down Expand Up @@ -209,7 +204,7 @@ component = H.mkComponent
H.modify_ _ { compiled = Just res }

HandleEditor (Editor.TextChanged text) -> do
_ <- H.fork $ handleAction $ Cache text
_ <- H.fork $ handleAction $ EncodeInURL text
{ autoCompile } <- H.gets _.settings
when autoCompile $ handleAction $ Compile $ Just text

Expand Down Expand Up @@ -340,6 +335,7 @@ component = H.mkComponent
]
[ HH.text "Show JS" ]
]
, HH.slot_ (Proxy :: _ "shareButton") unit shareButton unit
, HH.li
[ HP.class_ $ HH.ClassName "menu-item" ]
[ HH.a
Expand Down Expand Up @@ -442,6 +438,48 @@ renderCompilerErrors errors = do
, renderPlaintext message
]

type ShareButtonState =
{ forkId :: Maybe H.ForkId
, showCopySucceeded :: Maybe Boolean
}

shareButton :: forall q i o. H.Component q i o Aff
shareButton = H.mkComponent
{ initialState: \_ -> { forkId: Nothing, showCopySucceeded: Nothing }
, render
, eval: H.mkEval $ H.defaultEval
{ handleAction = handleAction
}
}
where
handleAction :: Unit -> H.HalogenM ShareButtonState Unit () o Aff Unit
handleAction _ = do
H.gets _.forkId >>= traverse_ H.kill
url <- H.liftEffect $ window >>= location >>= href
copySucceeded <- H.liftAff $ makeAff \f -> do
runEffectFn3 copyToClipboard url (f (Right true)) (f (Right false))
mempty
forkId <- H.fork do
H.liftAff $ delay (1_500.0 # Milliseconds)
H.modify_ _ { showCopySucceeded = Nothing }
H.put { showCopySucceeded: Just copySucceeded, forkId: Just forkId }
render :: ShareButtonState -> H.ComponentHTML Unit () Aff
render { showCopySucceeded } = do
let
message = case showCopySucceeded of
Just true -> "️✅ Copied to clipboard"
Just false -> "️❌ Failed to copy"
Nothing -> "Share URL"
HH.li
[ HP.class_ $ HH.ClassName "menu-item no-mobile" ]
[ HH.label
[ HP.id "share_label"
, HP.title "Share URL"
, HE.onClick \_ -> unit
]
[ HH.text message ]
]

menuRadio
:: forall w
. { name :: String
Expand Down Expand Up @@ -505,13 +543,13 @@ toAnnotation markerType { position, message } =
, text: message
}

withSession :: String -> Aff { sourceFile :: Maybe SourceFile, code :: String }
withSession sessionId = do
state <- H.liftEffect $ tryRetrieveSession sessionId
githubId <- H.liftEffect $ getQueryStringMaybe "github"
withSession :: Aff { sourceFile :: Maybe SourceFile, code :: String }
withSession = do
Comment on lines +546 to +547
Copy link
Contributor Author

@pete-murphy pete-murphy Aug 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also maybe the withSession name no longer makes sense, now that this doesn't take a sessionId

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are your thoughts on what this could be renamed to then?

Copy link
Contributor Author

@pete-murphy pete-murphy Sep 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't think of anything better 😅 so I'll just leave it

state <- H.liftEffect $ getQueryStringMaybe "code"
githubId <- H.liftEffect $ getQueryStringMaybe "github"
gistId <- H.liftEffect $ getQueryStringMaybe "gist"
code <- case state of
Just { code } -> pure code
code <- case state >>= decompressFromEncodedURIComponent of
Just code -> pure code
Nothing -> do
let
action = oneOf
Expand Down
5 changes: 5 additions & 0 deletions client/src/Try/QueryString.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
export {
compressToEncodedURIComponent,
decompressFromEncodedURIComponent as decompressFromEncodedURIComponent_,
} from 'lz-string';

export function getQueryString() {
return window.location.search;
}
Expand Down
12 changes: 12 additions & 0 deletions client/src/Try/QueryString.purs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ module Try.QueryString
, getQueryStringMaybe
, setQueryString
, setQueryStrings
, compressToEncodedURIComponent
, decompressFromEncodedURIComponent
) where

import Prelude

import Data.Array as Array
import Data.Maybe (Maybe(..))
import Data.Newtype (wrap)
import Data.Nullable (Nullable, toMaybe)
import Data.String as String
import Data.Tuple (Tuple(..))
import Effect (Effect)
Expand Down Expand Up @@ -58,3 +61,12 @@ setQueryStrings :: Object.Object String -> Effect Unit
setQueryStrings ss = do
params <- getQueryParams
runEffectFn1 setQueryParameters (Object.union ss params)

-- | Compress a string to a URI-encoded string using LZ-based compression algorithm
foreign import compressToEncodedURIComponent :: String -> String

foreign import decompressFromEncodedURIComponent_ :: String -> Nullable String

-- | Decompress a string from a URI-encoded string using LZ-based compression algorithm
decompressFromEncodedURIComponent :: String -> Maybe String
decompressFromEncodedURIComponent = toMaybe <$> decompressFromEncodedURIComponent_
16 changes: 0 additions & 16 deletions client/src/Try/Session.js

This file was deleted.

55 changes: 0 additions & 55 deletions client/src/Try/Session.purs

This file was deleted.