diff --git a/app/soapbox/components/loading_spinner.js b/app/soapbox/components/loading_spinner.js
new file mode 100644
index 000000000..a632454db
--- /dev/null
+++ b/app/soapbox/components/loading_spinner.js
@@ -0,0 +1,20 @@
+/**
+ * iOS style loading spinner.
+ * It's mostly CSS, adapted from: https://loading.io/css/
+ */
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const LoadingSpinner = ({ size = 30 }) => (
+
+ {Array(12).fill().map(i => (
+
+ ))}
+
+);
+
+LoadingSpinner.propTypes = {
+ size: PropTypes.number,
+};
+
+export default LoadingSpinner;
diff --git a/app/soapbox/components/pull_to_refresh.js b/app/soapbox/components/pull_to_refresh.js
index 6d315e687..f138e14ae 100644
--- a/app/soapbox/components/pull_to_refresh.js
+++ b/app/soapbox/components/pull_to_refresh.js
@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import PTRComponent from 'react-simple-pull-to-refresh';
+import LoadingSpinner from 'soapbox/components/loading_spinner';
/**
* PullToRefresh:
@@ -32,7 +33,10 @@ export default class PullToRefresh extends React.Component {
onRefresh={this.handleRefresh}
pullingContent={null}
// `undefined` will fallback to the default, while `null` will render nothing
- refreshingContent={onRefresh ? undefined : null}
+ refreshingContent={onRefresh ? : null}
+ pullDownThreshold={67}
+ maxPullDownDistance={95}
+ resistance={2}
{...rest}
>
{children}
diff --git a/app/styles/loading.scss b/app/styles/loading.scss
index c26ec70fe..b893128ae 100644
--- a/app/styles/loading.scss
+++ b/app/styles/loading.scss
@@ -229,3 +229,114 @@
.ptr__children {
overflow: visible !important;
}
+
+.ptr .lds-spinner {
+ width: 40px;
+ height: 40px;
+}
+
+.ptr__pull-down {
+ transform: translateY(10px);
+}
+
+/**
+ * iOS style loading spinner.
+ * Adapted from: https://loading.io/css/
+ * With some help scaling it: https://signalvnoise.com/posts/2577-loading-spinner-animation-using-css-and-webkit
+ */
+.lds-spinner {
+ display: inline-block;
+ position: relative;
+ width: 80px;
+ height: 80px;
+
+ div {
+ position: absolute;
+ transform-origin: 50% 50%;
+ animation: lds-spinner 1.2s linear infinite;
+ width: 100%;
+ height: 100%;
+
+ &::after {
+ content: ' ';
+ display: block;
+ position: absolute;
+ top: 3.75%;
+ left: 46.25%;
+ width: 7.5%;
+ height: 22.5%;
+ border-radius: 20%;
+ background: var(--primary-text-color);
+ }
+
+ &:nth-child(1) {
+ transform: rotate(0deg);
+ animation-delay: -1.1s;
+ }
+
+ &:nth-child(2) {
+ transform: rotate(30deg);
+ animation-delay: -1s;
+ }
+
+ &:nth-child(3) {
+ transform: rotate(60deg);
+ animation-delay: -0.9s;
+ }
+
+ &:nth-child(4) {
+ transform: rotate(90deg);
+ animation-delay: -0.8s;
+ }
+
+ &:nth-child(5) {
+ transform: rotate(120deg);
+ animation-delay: -0.7s;
+ }
+
+ &:nth-child(6) {
+ transform: rotate(150deg);
+ animation-delay: -0.6s;
+ }
+
+ &:nth-child(7) {
+ transform: rotate(180deg);
+ animation-delay: -0.5s;
+ }
+
+ &:nth-child(8) {
+ transform: rotate(210deg);
+ animation-delay: -0.4s;
+ }
+
+ &:nth-child(9) {
+ transform: rotate(240deg);
+ animation-delay: -0.3s;
+ }
+
+ &:nth-child(10) {
+ transform: rotate(270deg);
+ animation-delay: -0.2s;
+ }
+
+ &:nth-child(11) {
+ transform: rotate(300deg);
+ animation-delay: -0.1s;
+ }
+
+ &:nth-child(12) {
+ transform: rotate(330deg);
+ animation-delay: 0s;
+ }
+ }
+}
+
+@keyframes lds-spinner {
+ 0% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ }
+}