Merge branch 'main' into release

This commit is contained in:
xmflsct 2023-02-09 14:37:12 +01:00
commit c46f03ed37
74 changed files with 1829 additions and 1130 deletions

View File

@ -1,363 +0,0 @@
diff --git a/RNFastImage.podspec b/RNFastImage.podspec
index db0fada63fc06191f8620d336d244edde6c3dba3..9c22c36f6978530da21afe143324ff79b4e96454 100644
--- a/RNFastImage.podspec
+++ b/RNFastImage.podspec
@@ -16,6 +16,6 @@ Pod::Spec.new do |s|
s.source_files = "ios/**/*.{h,m}"
s.dependency 'React-Core'
- s.dependency 'SDWebImage', '~> 5.11.1'
- s.dependency 'SDWebImageWebPCoder', '~> 0.8.4'
+ s.dependency 'SDWebImage', '~> 5.15.0'
+ s.dependency 'SDWebImageWebPCoder', '~> 0.9.1'
end
diff --git a/android/build.gradle b/android/build.gradle
index 5b21cd59c40a5754f5d19c77e2a0eb0229925911..19d82f826e88125c5e6d87ee7c348fac621f548c 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -65,4 +65,5 @@ dependencies {
implementation "com.github.bumptech.glide:glide:${glideVersion}"
implementation "com.github.bumptech.glide:okhttp3-integration:${glideVersion}"
annotationProcessor "com.github.bumptech.glide:compiler:${glideVersion}"
+ implementation 'com.github.penfeizhou.android.animation:glide-plugin:2.12.0'
}
diff --git a/android/src/main/java/com/dylanvann/fastimage/FastImageEnterTransition.java b/android/src/main/java/com/dylanvann/fastimage/FastImageEnterTransition.java
new file mode 100644
index 0000000000000000000000000000000000000000..55e3b4e0d463654f62d942ba05c2a5e51ae9d6d7
--- /dev/null
+++ b/android/src/main/java/com/dylanvann/fastimage/FastImageEnterTransition.java
@@ -0,0 +1,6 @@
+package com.dylanvann.fastimage;
+
+public enum FastImageEnterTransition {
+ TRANSITION_NONE,
+ FADE_IN
+}
diff --git a/android/src/main/java/com/dylanvann/fastimage/FastImageTransitions.java b/android/src/main/java/com/dylanvann/fastimage/FastImageTransitions.java
new file mode 100644
index 0000000000000000000000000000000000000000..d764cc4b8d110f087120a4f0dc5d986754806dec
--- /dev/null
+++ b/android/src/main/java/com/dylanvann/fastimage/FastImageTransitions.java
@@ -0,0 +1,20 @@
+package com.dylanvann.fastimage;
+
+import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
+import com.bumptech.glide.TransitionOptions;
+import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
+import android.view.animation.DecelerateInterpolator;
+
+public class FastImageTransitions {
+ static final DecelerateInterpolator mInterpolator = new DecelerateInterpolator();
+
+ public static TransitionOptions getEnterTransition(FastImageEnterTransition transition, int duration) {
+ switch (transition) {
+ case FADE_IN:
+ return DrawableTransitionOptions.withCrossFade(duration);
+
+ default:
+ throw new JSApplicationIllegalArgumentException("FastImage, invalid enterTransition argument");
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/java/com/dylanvann/fastimage/FastImageViewConverter.java b/android/src/main/java/com/dylanvann/fastimage/FastImageViewConverter.java
index 86ca00d018d7ded0edff733373d80976c8dbb961..e6220f57b38a3fe3ae9d5a75228f791e0ec978bb 100644
--- a/android/src/main/java/com/dylanvann/fastimage/FastImageViewConverter.java
+++ b/android/src/main/java/com/dylanvann/fastimage/FastImageViewConverter.java
@@ -50,6 +50,12 @@ class FastImageViewConverter {
put("center", ScaleType.CENTER_INSIDE);
}};
+ private static final Map<String, FastImageEnterTransition> FAST_IMAGE_ENTER_TRANSITION_MAP =
+ new HashMap<String, FastImageEnterTransition>() {{
+ put("none", FastImageEnterTransition.TRANSITION_NONE);
+ put("fadeIn", FastImageEnterTransition.FADE_IN);
+ }};
+
// Resolve the source uri to a file path that android understands.
static @Nullable
FastImageSource getImageSource(Context context, @Nullable ReadableMap source) {
@@ -125,6 +131,10 @@ class FastImageViewConverter {
return getValueFromSource("cache", "immutable", FAST_IMAGE_CACHE_CONTROL_MAP, source);
}
+ static FastImageEnterTransition getEnterTransition(String propValue) {
+ return getValue("enterTransition", "none", FAST_IMAGE_ENTER_TRANSITION_MAP, propValue);
+ }
+
private static Priority getPriority(ReadableMap source) {
return getValueFromSource("priority", "normal", FAST_IMAGE_PRIORITY_MAP, source);
}
diff --git a/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java b/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java
index c7a795471c8f8b48163c778836406bc5ead75dab..53b481547b44224e7791a8d3f39815c9c9a4be59 100644
--- a/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java
+++ b/android/src/main/java/com/dylanvann/fastimage/FastImageViewManager.java
@@ -83,6 +83,17 @@ class FastImageViewManager extends SimpleViewManager<FastImageViewWithUrl> imple
view.setScaleType(scaleType);
}
+ @ReactProp(name = "enterTransition")
+ public void setEnterTransition(FastImageViewWithUrl view, String enterTransition) {
+ final FastImageEnterTransition transition = FastImageViewConverter.getEnterTransition(enterTransition);
+ view.setEnterTransition(transition);
+ }
+
+ @ReactProp(name = "transitionDuration")
+ public void setTransitionDuration(FastImageViewWithUrl view, int transitionDuration) {
+ view.setTransitionDuration(transitionDuration);
+ }
+
@Override
public void onDropViewInstance(@NonNull FastImageViewWithUrl view) {
// This will cancel existing requests.
diff --git a/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java b/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java
index 34fcf898d17d82fd52375e9028b71ad815b9b15b..fd57ac68de093d2a8ee53aeede45328c8d52aa39 100644
--- a/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java
+++ b/android/src/main/java/com/dylanvann/fastimage/FastImageViewWithUrl.java
@@ -30,6 +30,8 @@ class FastImageViewWithUrl extends AppCompatImageView {
private boolean mNeedsReload = false;
private ReadableMap mSource = null;
private Drawable mDefaultSource = null;
+ private FastImageEnterTransition mEnterTransition = FastImageEnterTransition.TRANSITION_NONE;
+ private int mTransitionDuration = 350;
public GlideUrl glideUrl;
@@ -47,6 +49,14 @@ class FastImageViewWithUrl extends AppCompatImageView {
mDefaultSource = source;
}
+ public void setEnterTransition(@Nullable FastImageEnterTransition transition) {
+ mEnterTransition = transition;
+ }
+
+ public void setTransitionDuration(int duration) {
+ mTransitionDuration = duration == 0 ? 350 : duration;
+ }
+
private boolean isNullOrEmpty(final String url) {
return url == null || url.trim().isEmpty();
}
@@ -147,6 +157,10 @@ class FastImageViewWithUrl extends AppCompatImageView {
if (key != null)
builder.listener(new FastImageRequestListener(key));
+ if (mEnterTransition != FastImageEnterTransition.TRANSITION_NONE) {
+ builder.transition(FastImageTransitions.getEnterTransition(mEnterTransition, mTransitionDuration));
+ }
+
builder.into(this);
}
}
diff --git a/dist/index.cjs.js b/dist/index.cjs.js
index 2df6a29769978d8d947dfb50b422e1f56bd97fb6..f3904e20edac5f19cc26f41a4ff02eecd73ac627 100644
--- a/dist/index.cjs.js
+++ b/dist/index.cjs.js
@@ -9,6 +9,10 @@ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'defau
var _extends__default = /*#__PURE__*/_interopDefaultLegacy(_extends);
var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
+const enterTransition = {
+ none: 'none',
+ fadeIn: 'fadeIn'
+}
const resizeMode = {
contain: 'contain',
cover: 'cover',
@@ -115,6 +119,7 @@ const FastImageComponent = /*#__PURE__*/React.forwardRef((props, ref) => /*#__PU
}, props)));
FastImageComponent.displayName = 'FastImage';
const FastImage = FastImageComponent;
+FastImage.enterTransition = enterTransition
FastImage.resizeMode = resizeMode;
FastImage.cacheControl = cacheControl;
FastImage.priority = priority;
diff --git a/dist/index.d.ts b/dist/index.d.ts
index 5abb7c98b767cd0709b53f5ab2dd50c752a9377b..2da22817e3136673d40a177ae8c9fc2209f143d8 100644
--- a/dist/index.d.ts
+++ b/dist/index.d.ts
@@ -1,5 +1,10 @@
import React from 'react';
import { FlexStyle, LayoutChangeEvent, ShadowStyleIOS, StyleProp, TransformsStyle, ImageRequireSource, AccessibilityProps, ViewProps, ColorValue } from 'react-native';
+export declare type EnterTransition = 'none' | 'fadeIn';
+declare const enterTransition: {
+ readonly none: "none";
+ readonly fadeIn: "fadeIn";
+};
export declare type ResizeMode = 'contain' | 'cover' | 'stretch' | 'center';
declare const resizeMode: {
readonly contain: "contain";
@@ -57,6 +62,16 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
defaultSource?: ImageRequireSource;
resizeMode?: ResizeMode;
fallback?: boolean;
+ /**
+ * Transition durations.
+ * @default none
+ */
+ enterTransition?: EnterTransition
+ /**
+ * Enter transition duration in ms.
+ * @default 500ms
+ */
+ transitionDuration?: number
onLoadStart?(): void;
onProgress?(event: OnProgressEvent): void;
onLoad?(event: OnLoadEvent): void;
@@ -91,6 +106,7 @@ export interface FastImageProps extends AccessibilityProps, ViewProps {
children?: React.ReactNode;
}
export interface FastImageStaticProperties {
+ enterTransition: typeof enterTransition;
resizeMode: typeof resizeMode;
priority: typeof priority;
cacheControl: typeof cacheControl;
diff --git a/dist/index.js b/dist/index.js
index 58e0308bd44836aad3e4979b5c1151083956c295..5853b3b2fd05c91be8c70819fe6fc45606f26f8d 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -2,6 +2,10 @@ import _extends from '@babel/runtime/helpers/extends';
import React, { forwardRef, memo } from 'react';
import { NativeModules, StyleSheet, requireNativeComponent, Image, View, Platform } from 'react-native';
+const enterTransition = {
+ none: 'none',
+ fadeIn: 'fadeIn'
+}
const resizeMode = {
contain: 'contain',
cover: 'cover',
@@ -57,6 +61,8 @@ function FastImageBase({
children,
// eslint-disable-next-line no-shadow
resizeMode = 'cover',
+ enterTransition = 'none',
+ transitionDuration = 350,
forwardedRef,
...props
}) {
@@ -79,7 +85,9 @@ function FastImageBase({
onLoad: onLoad,
onError: onError,
onLoadEnd: onLoadEnd,
- resizeMode: resizeMode
+ resizeMode: resizeMode,
+ enterTransition: enterTransition,
+ transitionDuration: transitionDuration
})), children);
}
@@ -98,7 +106,9 @@ function FastImageBase({
onFastImageLoad: onLoad,
onFastImageError: onError,
onFastImageLoadEnd: onLoadEnd,
- resizeMode: resizeMode
+ resizeMode: resizeMode,
+ enterTransition: enterTransition,
+ transitionDuration: transitionDuration
})), children);
}
@@ -108,6 +118,7 @@ const FastImageComponent = /*#__PURE__*/forwardRef((props, ref) => /*#__PURE__*/
}, props)));
FastImageComponent.displayName = 'FastImage';
const FastImage = FastImageComponent;
+FastImage.enterTransition = enterTransition
FastImage.resizeMode = resizeMode;
FastImage.cacheControl = cacheControl;
FastImage.priority = priority;
diff --git a/ios/FastImage/FFFastImageView.h b/ios/FastImage/FFFastImageView.h
index e52fca79882ad2a678487a46b2fe158427e06f3a..6c9c41b0b1a3c967a3715a24bb692447b76ef365 100644
--- a/ios/FastImage/FFFastImageView.h
+++ b/ios/FastImage/FFFastImageView.h
@@ -7,6 +7,7 @@
#import <React/RCTResizeMode.h>
#import "FFFastImageSource.h"
+#import "FFFastImageViewManager.h"
@interface FFFastImageView : SDAnimatedImageView
@@ -16,6 +17,8 @@
@property (nonatomic, copy) RCTDirectEventBlock onFastImageLoad;
@property (nonatomic, copy) RCTDirectEventBlock onFastImageLoadEnd;
@property (nonatomic, assign) RCTResizeMode resizeMode;
+@property (nonatomic, assign) FFFEnterTransition enterTransition;
+@property (nonatomic, assign) NSTimeInterval transitionDuration;
@property (nonatomic, strong) FFFastImageSource *source;
@property (nonatomic, strong) UIImage *defaultSource;
@property (nonatomic, strong) UIColor *imageColor;
diff --git a/ios/FastImage/FFFastImageView.m b/ios/FastImage/FFFastImageView.m
index f7100815e652539b29b1fa70ff1477c5f5db08dc..ecb79eafe566fe52090adada3cdf16eb10a67513 100644
--- a/ios/FastImage/FFFastImageView.m
+++ b/ios/FastImage/FFFastImageView.m
@@ -71,6 +71,18 @@ - (void) setImageColor: (UIColor*)imageColor {
}
}
+- (void) setTransitionDuration: (NSTimeInterval)transitionDuration {
+ self.sd_imageTransition.duration = transitionDuration;
+}
+
+- (void) setEnterTransition: (FFFEnterTransition)enterTransition {
+ switch (enterTransition) {
+ case FFFFadeIn:
+ self.sd_imageTransition = SDWebImageTransition.fadeTransition;
+ break;
+ }
+}
+
- (UIImage*) makeImage: (UIImage*)image withTint: (UIColor*)color {
UIImage* newImage = [image imageWithRenderingMode: UIImageRenderingModeAlwaysTemplate];
UIGraphicsBeginImageContextWithOptions(image.size, NO, newImage.scale);
diff --git a/ios/FastImage/FFFastImageViewManager.h b/ios/FastImage/FFFastImageViewManager.h
index 8ba6020e2c6e5757ed778d00e3f43a6ff4c1d50a..a269669301ea00ef3c2714123d17e822094635d6 100644
--- a/ios/FastImage/FFFastImageViewManager.h
+++ b/ios/FastImage/FFFastImageViewManager.h
@@ -1,5 +1,10 @@
#import <React/RCTViewManager.h>
+typedef NS_ENUM(NSInteger, FFFEnterTransition) {
+ FFFTransitionNone,
+ FFFFadeIn,
+};
+
@interface FFFastImageViewManager : RCTViewManager
@end
diff --git a/ios/FastImage/FFFastImageViewManager.m b/ios/FastImage/FFFastImageViewManager.m
index 84ca94e26e546d4d139dabca6c3efd0a890eda63..2184bac31f0d547e6119356bb4fc7931be87446d 100644
--- a/ios/FastImage/FFFastImageViewManager.m
+++ b/ios/FastImage/FFFastImageViewManager.m
@@ -13,6 +13,8 @@ - (FFFastImageView*)view {
}
RCT_EXPORT_VIEW_PROPERTY(source, FFFastImageSource)
+RCT_EXPORT_VIEW_PROPERTY(enterTransition, FFFEnterTransition)
+RCT_EXPORT_VIEW_PROPERTY(transitionDuration, NSTimeInterval)
RCT_EXPORT_VIEW_PROPERTY(defaultSource, UIImage)
RCT_EXPORT_VIEW_PROPERTY(resizeMode, RCTResizeMode)
RCT_EXPORT_VIEW_PROPERTY(onFastImageLoadStart, RCTDirectEventBlock)
diff --git a/ios/FastImage/RCTConvert+FFFastImage.m b/ios/FastImage/RCTConvert+FFFastImage.m
index 43f8922157655a7497f56a3909ef6b2a886f07d8..0705f8e05f44f3053e7239fcc9a30d986e7aaab7 100644
--- a/ios/FastImage/RCTConvert+FFFastImage.m
+++ b/ios/FastImage/RCTConvert+FFFastImage.m
@@ -1,5 +1,6 @@
#import "RCTConvert+FFFastImage.h"
#import "FFFastImageSource.h"
+#import "FFFastImageViewManager.h"
@implementation RCTConvert (FFFastImage)
@@ -15,6 +16,11 @@ @implementation RCTConvert (FFFastImage)
@"cacheOnly": @(FFFCacheControlCacheOnly),
}), FFFCacheControlImmutable, integerValue);
+RCT_ENUM_CONVERTER(FFFEnterTransition, (@{
+ @"none": @(FFFTransitionNone),
+ @"fadeIn": @(FFFFadeIn),
+ }), FFFTransitionNone, integerValue);
+
+ (FFFastImageSource *)FFFastImageSource:(id)json {
if (!json) {
return nil;

View File

@ -3,14 +3,14 @@ PODS:
- DoubleConversion (1.1.6)
- EXApplication (5.0.1):
- ExpoModulesCore
- EXAV (13.1.0):
- EXAV (13.2.0):
- ExpoModulesCore
- ReactCommon/turbomodule/core
- EXConstants (14.1.0):
- EXConstants (14.2.0):
- ExpoModulesCore
- EXErrorRecovery (4.0.1):
- ExpoModulesCore
- EXFileSystem (15.1.1):
- EXFileSystem (15.2.0):
- ExpoModulesCore
- EXFont (11.0.1):
- ExpoModulesCore
@ -18,31 +18,37 @@ PODS:
- ExpoModulesCore
- Expo (47.0.13):
- ExpoModulesCore
- ExpoCrypto (12.1.0):
- ExpoCrypto (12.2.0):
- ExpoModulesCore
- ExpoHaptics (12.1.0):
- ExpoHaptics (12.2.0):
- ExpoModulesCore
- ExpoImage (1.0.0-beta.6):
- ExpoModulesCore
- SDWebImage (~> 5.15.0)
- SDWebImageAVIFCoder (~> 0.9.4)
- SDWebImageSVGCoder (~> 1.6.1)
- SDWebImageWebPCoder (~> 0.9.1)
- ExpoKeepAwake (11.0.1):
- ExpoModulesCore
- ExpoLocalization (14.0.0):
- ExpoLocalization (14.1.0):
- ExpoModulesCore
- ExpoModulesCore (1.1.1):
- React-Core
- ReactCommon/turbomodule/core
- ExpoRandom (13.0.0):
- ExpoRandom (13.1.0):
- ExpoModulesCore
- ExpoStoreReview (6.1.0):
- ExpoStoreReview (6.2.0):
- ExpoModulesCore
- ExpoVideoThumbnails (7.1.0):
- ExpoVideoThumbnails (7.2.0):
- ExpoModulesCore
- ExpoWebBrowser (12.0.0):
- ExpoModulesCore
- EXScreenCapture (5.0.0):
- EXScreenCapture (5.1.0):
- ExpoModulesCore
- EXScreenOrientation (5.0.1):
- EXScreenOrientation (5.1.0):
- ExpoModulesCore
- React-Core
- EXSecureStore (12.0.0):
- EXSecureStore (12.1.0):
- ExpoModulesCore
- EXSplashScreen (0.17.5):
- ExpoModulesCore
@ -58,7 +64,16 @@ PODS:
- fmt (6.2.1)
- glog (0.3.5)
- hermes-engine (0.70.7)
- libaom (2.0.2):
- libvmaf
- libavif (0.10.1):
- libavif/libaom (= 0.10.1)
- libavif/core (0.10.1)
- libavif/libaom (0.10.1):
- libaom (>= 2.0.0)
- libavif/core
- libevent (2.1.12)
- libvmaf (2.2.0)
- libwebp (1.2.4):
- libwebp/demux (= 1.2.4)
- libwebp/mux (= 1.2.4)
@ -299,8 +314,6 @@ PODS:
- glog
- react-native-blur (4.3.0):
- React-Core
- react-native-blurhash (1.1.10):
- React-Core
- react-native-cameraroll (5.2.3):
- React-Core
- react-native-image-picker (5.0.1):
@ -401,10 +414,6 @@ PODS:
- React-Core
- RNCClipboard (1.11.1):
- React-Core
- RNFastImage (8.6.3):
- React-Core
- SDWebImage (~> 5.15.0)
- SDWebImageWebPCoder (~> 0.9.1)
- RNGestureHandler (2.9.0):
- React-Core
- RNReanimated (2.14.4):
@ -447,6 +456,11 @@ PODS:
- SDWebImage (5.15.0):
- SDWebImage/Core (= 5.15.0)
- SDWebImage/Core (5.15.0)
- SDWebImageAVIFCoder (0.9.5):
- libavif (>= 0.9.1)
- SDWebImage (~> 5.10)
- SDWebImageSVGCoder (1.6.1):
- SDWebImage/Core (~> 5.6)
- SDWebImageWebPCoder (0.9.1):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.13)
@ -467,6 +481,7 @@ DEPENDENCIES:
- Expo (from `../node_modules/expo`)
- ExpoCrypto (from `../node_modules/expo-crypto/ios`)
- ExpoHaptics (from `../node_modules/expo-haptics/ios`)
- ExpoImage (from `../node_modules/expo-image/ios`)
- ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`)
- ExpoLocalization (from `../node_modules/expo-localization/ios`)
- ExpoModulesCore (from `../node_modules/expo-modules-core`)
@ -500,7 +515,6 @@ DEPENDENCIES:
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
- "react-native-blur (from `../node_modules/@react-native-community/blur`)"
- react-native-blurhash (from `../node_modules/react-native-blurhash`)
- "react-native-cameraroll (from `../node_modules/@react-native-camera-roll/camera-roll`)"
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
- react-native-ios-context-menu (from `../node_modules/react-native-ios-context-menu`)
@ -527,7 +541,6 @@ DEPENDENCIES:
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
- "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)"
- RNFastImage (from `../node_modules/react-native-fast-image`)
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
- RNReanimated (from `../node_modules/react-native-reanimated`)
- RNScreens (from `../node_modules/react-native-screens`)
@ -539,11 +552,16 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- fmt
- libaom
- libavif
- libevent
- libvmaf
- libwebp
- MMKV
- MMKVCore
- SDWebImage
- SDWebImageAVIFCoder
- SDWebImageSVGCoder
- SDWebImageWebPCoder
- Sentry
- Swime
@ -573,6 +591,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/expo-crypto/ios"
ExpoHaptics:
:path: "../node_modules/expo-haptics/ios"
ExpoImage:
:path: "../node_modules/expo-image/ios"
ExpoKeepAwake:
:path: "../node_modules/expo-keep-awake/ios"
ExpoLocalization:
@ -635,8 +655,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/logger"
react-native-blur:
:path: "../node_modules/@react-native-community/blur"
react-native-blurhash:
:path: "../node_modules/react-native-blurhash"
react-native-cameraroll:
:path: "../node_modules/@react-native-camera-roll/camera-roll"
react-native-image-picker:
@ -689,8 +707,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/@react-native-async-storage/async-storage"
RNCClipboard:
:path: "../node_modules/@react-native-clipboard/clipboard"
RNFastImage:
:path: "../node_modules/react-native-fast-image"
RNGestureHandler:
:path: "../node_modules/react-native-gesture-handler"
RNReanimated:
@ -710,32 +726,36 @@ SPEC CHECKSUMS:
boost: a7c83b31436843459a1961bfd74b96033dc77234
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
EXApplication: 034b1c40a8e9fe1bff76a1e511ee90dff64ad834
EXAV: 4b92292fb107520a25956bea940a94a3bb4911ca
EXConstants: 44f7d347d0432a66f469d0ce1dc4e3a0ca1b8b2d
EXAV: 1242c4c206fc522058a2749019064e979a4c0b76
EXConstants: 397186c7e312c33eb1ab85fa1f434dc123778136
EXErrorRecovery: ae43433feb0608a64dc5b1c8363b3e7769a9ea24
EXFileSystem: 60602b6eefa6873f97172c684b7537c9760b50d6
EXFileSystem: d9fea7fe7a4390a0ef226cac33958de9178388b9
EXFont: 319606bfe48c33b5b5063fb0994afdc496befe80
EXNotifications: babce2a87b7922051354fcfe7a74dd279b7e272a
Expo: b9fa98bf260992312ee3c424400819fb9beadafe
ExpoCrypto: 6eb2a5ede7d95b7359a5f0391ee0c5d2ecd144b3
ExpoHaptics: 129d3f8d44c2205adcdf8db760602818463d5437
ExpoCrypto: 98c71864077c4d0fe798a6a5aee1a8c1294cef85
ExpoHaptics: 97c532f311c3e638c14a6134f23564d007b76de4
ExpoImage: 748f2b8d3974f1d51c7706fd61057b93241738aa
ExpoKeepAwake: 69b59d0a8d2b24de9f82759c39b3821fec030318
ExpoLocalization: e202d1e2a4950df17ac8d0889d65a1ffd7532d7e
ExpoLocalization: 28ce7cfa174a752f7ace84189710f1385373655b
ExpoModulesCore: 485dff3a59b036a33b6050c0a5aea3cf1037fdd1
ExpoRandom: 58b7e0a5fe1adf1cb6dc1cbe503a6fe9524f36ce
ExpoStoreReview: 713336ff504db3a6983475bf7c67519cc5efc86f
ExpoVideoThumbnails: 424db02cedfbbe2d498bcb2712ea4ba8a9dcb453
ExpoRandom: d8fc05d0d071485b06a97ab2a78cb7f8082052cd
ExpoStoreReview: e96ba0690ea21dc5d341cfafd0b26bac7bc974f5
ExpoVideoThumbnails: 865fa65f2b4f006ff02ef9e3e9c10370d9442d0a
ExpoWebBrowser: 073e50f16669d498fb49063b9b7fe780b24f7fda
EXScreenCapture: d9f1ec31042dfef109290d06c2b4789b7444d16d
EXScreenOrientation: 07e5aeff07bce09a2b214981e612d87fd7719997
EXSecureStore: daec0117c922a67c658cb229152a9e252e5c1750
EXScreenCapture: bcf94c8199cd1876166e384b2398ff519a8ef7ee
EXScreenOrientation: d43067a93e75234a7ce5154e2759fff2238dbfd5
EXSecureStore: ec150f49b22269022c6184f1711abb05fe98d72d
EXSplashScreen: 3e989924f61a8dd07ee4ea584c6ba14be9b51949
FBLazyVector: a6454570f573a0f6f1d397e5a95c13e8e45d1700
FBReactNativeSpec: 09e8dfba44487e5dc4882a9f5318cde67549549c
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
hermes-engine: 566e656aa95456a3f3f739fd76ea9a9656f2633f
libaom: 9bb51e0f8f9192245e3ca2a1c9e4375d9cbccc52
libavif: e242998ccec1c83bcba0bbdc256f460ad5077348
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
libvmaf: 8d61aabc2f4ed3e6591cf7406fa00a223ec11289
libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
MMKV: 7f34558bbb5a33b0eaefae2de4b6a20a2ffdad6f
MMKVCore: ddf41b9d9262f058419f9ba7598719af56c02cd3
@ -755,7 +775,6 @@ SPEC CHECKSUMS:
React-jsinspector: 1c34fea1868136ecde647bc11fae9266d4143693
React-logger: e9f407f9fdf3f3ce7749ae6f88affe63e8446019
react-native-blur: 50c9feabacbc5f49b61337ebc32192c6be7ec3c3
react-native-blurhash: add4df9a937b4e021a24bc67a0714f13e0bd40b7
react-native-cameraroll: 5b25d0be40185d02e522bf2abf8a1ba4e8faa107
react-native-image-picker: 8cb4280e2c1efc3daeb2d9d597f9429a60472e40
react-native-ios-context-menu: e529171ba760a1af7f2ef0729f5a7f4d226171c5
@ -782,7 +801,6 @@ SPEC CHECKSUMS:
ReactCommon: 0253d197eaa7f6689dcd3e7d5360449ab93e10df
RNCAsyncStorage: 8616bd5a58af409453ea4e1b246521bb76578d60
RNCClipboard: 2834e1c4af68697089cdd455ee4a4cdd198fa7dd
RNFastImage: bd611b5635f1e0f43c8ccf597b1ef6ee0d0f966d
RNGestureHandler: 071d7a9ad81e8b83fe7663b303d132406a7d8f39
RNReanimated: 6668b0587bebd4b15dd849b99e5a9c70fc12ed95
RNScreens: ea4cd3a853063cda19a4e3c28d2e52180c80f4eb
@ -790,6 +808,8 @@ SPEC CHECKSUMS:
RNShareMenu: cb9dac548c8bf147d06f0bf07296ad51ea9f5fc3
RNSVG: c1e76b81c76cdcd34b4e1188852892dc280eb902
SDWebImage: 9bec4c5cdd9579e1f57104735ee0c37df274d593
SDWebImageAVIFCoder: d759e21cf4efb640cc97250566aa556ad8bb877c
SDWebImageSVGCoder: 6fc109f9c2a82ab44510fff410b88b1a6c271ee8
SDWebImageWebPCoder: 18503de6621dd2c420d680e33d46bf8e1d5169b0
Sentry: 4c9babff9034785067c896fd580b1f7de44da020
Swime: d7b2c277503b6cea317774aedc2dce05613f8b0b

View File

@ -1,6 +1,6 @@
{
"name": "tooot",
"version": "4.8.7",
"version": "4.8.8",
"description": "tooot for Mastodon",
"author": "xmflsct <me@xmflsct.com>",
"license": "GPL-3.0-or-later",
@ -50,6 +50,7 @@
"expo-crypto": "^12.1.0",
"expo-file-system": "^15.1.1",
"expo-haptics": "^12.1.0",
"expo-image": "^1.0.0-beta.6",
"expo-linking": "^3.3.0",
"expo-localization": "^14.0.0",
"expo-notifications": "^0.17.0",
@ -70,14 +71,12 @@
"react-i18next": "^12.1.4",
"react-intl": "^6.2.7",
"react-native": "^0.70.7",
"react-native-blurhash": "^1.1.10",
"react-native-fast-image": "^8.6.3",
"react-native-flash-message": "^0.4.0",
"react-native-gesture-handler": "~2.9.0",
"react-native-image-picker": "^5.0.1",
"react-native-ios-context-menu": "^1.15.3",
"react-native-language-detection": "^0.2.2",
"react-native-mmkv": "^2.5.1",
"react-native-mmkv": "~2.5.1",
"react-native-pager-view": "^6.1.2",
"react-native-quick-base64": "^2.0.5",
"react-native-reanimated": "^2.14.4",
@ -116,7 +115,6 @@
},
"packageManager": "yarn@3.3.1",
"resolutions": {
"react-native-fast-image@^8.6.3": "patch:react-native-fast-image@npm%3A8.6.3#./.yarn/patches/react-native-fast-image-npm-8.6.3-03ee2d23c0.patch",
"expo-av@^13.0.2": "patch:expo-av@npm%3A13.0.2#./.yarn/patches/expo-av-npm-13.0.2-7a651776f1.patch",
"react-native-share-menu@^6.0.0": "patch:react-native-share-menu@npm%3A6.0.0#./.yarn/patches/react-native-share-menu-npm-6.0.0-f1094c3204.patch",
"@types/react-native-share-menu@^5.0.2": "patch:@types/react-native-share-menu@npm%3A5.0.2#./.yarn/patches/@types-react-native-share-menu-npm-5.0.2-373df17ecc.patch",

View File

@ -11,6 +11,7 @@ import log from '@utils/startup/log'
import netInfo from '@utils/startup/netInfo'
import push from '@utils/startup/push'
import sentry from '@utils/startup/sentry'
import { GLOBAL } from '@utils/storage'
import { getGlobalStorage, setAccount, setGlobalStorage } from '@utils/storage/actions'
import { migrateFromAsyncStorage, versionStorageGlobal } from '@utils/storage/migrations/toMMKV'
import ThemeManager from '@utils/styles/ThemeManager'
@ -24,10 +25,6 @@ import { enableFreeze } from 'react-native-screens'
import i18n from './i18n'
import Screens from './screens'
export const GLOBAL: { connect?: boolean } = {
connect: undefined
}
Platform.select({
android: LogBox.ignoreLogs(['Setting a timer for a long period of time'])
})

View File

@ -38,7 +38,7 @@ const ComponentAccount: React.FC<PropsWithChildren & Props> = ({ account, props,
<>
<View style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}>
<GracefullyImage
uri={{ original: account.avatar, static: account.avatar_static }}
sources={{ default: { uri: account.avatar }, static: { uri: account.avatar_static } }}
style={{
width: StyleConstants.Avatar.S,
height: StyleConstants.Avatar.S,

View File

@ -40,7 +40,7 @@ const AccountButton: React.FC<Props> = ({ account, additionalActions }) => {
}}
>
<GracefullyImage
uri={{ original: account.avatar_static }}
sources={{ default: { uri: account.avatar_static } }}
dimension={{
width: StyleConstants.Font.Size.L,
height: StyleConstants.Font.Size.L

View File

@ -8,6 +8,7 @@ import { getAccountStorage, setAccountStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation'
import { useTheme } from '@utils/styles/ThemeManager'
import { Image } from 'expo-image'
import { chunk } from 'lodash'
import React, { useContext, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -19,7 +20,6 @@ import {
TextInput,
View
} from 'react-native'
import FastImage from 'react-native-fast-image'
import EmojisContext from './Context'
const EmojisList = () => {
@ -129,9 +129,7 @@ const EmojisList = () => {
}}
style={{ padding: StyleConstants.Spacing.S }}
>
<FastImage
enterTransition='fadeIn'
transitionDuration={60}
<Image
accessibilityLabel={t('common:customEmoji.accessibilityLabel', {
emoji: emoji.shortcode
})}

View File

@ -1,36 +1,26 @@
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { connectMedia } from '@utils/api/helpers/connect'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect, useState } from 'react'
import {
AccessibilityProps,
Image,
Pressable,
StyleProp,
StyleSheet,
View,
ViewStyle
} from 'react-native'
import { Blurhash } from 'react-native-blurhash'
import FastImage, { ImageStyle } from 'react-native-fast-image'
// blurhas -> if blurhash, show before any loading succeed
// original -> load original
// original, remote -> if original failed, then remote
// preview, original -> first show preview, then original
// preview, original, remote -> first show preview, then original, if original failed, then remote
import { Image, ImageSource, ImageStyle } from 'expo-image'
import React, { useState } from 'react'
import { AccessibilityProps, Pressable, StyleProp, View, ViewStyle } from 'react-native'
export interface Props {
accessibilityLabel?: AccessibilityProps['accessibilityLabel']
accessibilityHint?: AccessibilityProps['accessibilityHint']
hidden?: boolean
uri: { preview?: string; original?: string; remote?: string; static?: string }
blurhash?: string
sources: {
preview?: ImageSource
default?: ImageSource
remote?: ImageSource
static?: ImageSource
blurhash?: string
}
dimension?: { width: number; height: number }
onPress?: () => void
style?: StyleProp<ViewStyle>
imageStyle?: StyleProp<ImageStyle>
imageStyle?: ImageStyle
// For image viewer when there is no image size available
setImageDimensions?: React.Dispatch<
React.SetStateAction<{
@ -39,49 +29,30 @@ export interface Props {
}>
>
dim?: boolean
enableLiveTextInteraction?: boolean
}
const GracefullyImage = ({
accessibilityLabel,
accessibilityHint,
hidden = false,
uri,
blurhash,
sources,
dimension,
onPress,
style,
imageStyle,
setImageDimensions,
dim
dim,
enableLiveTextInteraction = false
}: Props) => {
const { reduceMotionEnabled } = useAccessibility()
const { colors, theme } = useTheme()
const [imageLoaded, setImageLoaded] = useState(false)
const { theme } = useTheme()
const [currentUri, setCurrentUri] = useState<string | undefined>(uri.original || uri.remote)
const source: { uri?: string } = {
uri: reduceMotionEnabled && uri.static ? uri.static : currentUri
}
useEffect(() => {
if (
(uri.original ? currentUri !== uri.original : true) &&
(uri.remote ? currentUri !== uri.remote : true)
) {
setCurrentUri(uri.original || uri.remote)
}
}, [currentUri, uri.original, uri.remote])
const blurhashView = () => {
if (hidden || !imageLoaded) {
if (blurhash) {
return <Blurhash decodeAsync blurhash={blurhash} style={styles.placeholder} />
} else {
return <View style={[styles.placeholder, { backgroundColor: colors.shimmerDefault }]} />
}
} else {
return null
}
}
const [currentSource, setCurrentSource] = useState<ImageSource | undefined>(
sources.default || sources.remote
)
const source: ImageSource | undefined =
reduceMotionEnabled && sources.static ? sources.static : currentSource
return (
<Pressable
@ -91,50 +62,41 @@ const GracefullyImage = ({
style={[style, dimension]}
{...(onPress ? (hidden ? { disabled: true } : { onPress }) : { disabled: true })}
>
{uri.preview && !imageLoaded ? (
<FastImage
source={connectMedia({ uri: uri.preview })}
enterTransition='fadeIn'
transitionDuration={60}
style={[styles.placeholder]}
/>
) : null}
<FastImage
source={connectMedia(source)}
enterTransition={!blurhash && !uri.preview ? 'fadeIn' : 'none'}
transitionDuration={60}
style={[{ flex: 1 }, imageStyle]}
onLoad={() => {
setImageLoaded(true)
if (setImageDimensions && source.uri) {
Image.getSize(source.uri, (width, height) => setImageDimensions({ width, height }))
<Image
placeholderContentFit='cover'
placeholder={sources.blurhash || connectMedia(sources.preview)}
source={hidden ? undefined : connectMedia(source)}
transition={{ duration: 80 }}
style={{ flex: 1, ...imageStyle }}
onLoad={event => {
if (setImageDimensions && event.source) {
setImageDimensions(event.source)
}
}}
onError={() => {
if (uri.original && uri.original === currentUri && uri.remote) {
setCurrentUri(uri.remote)
if (
sources.default?.uri &&
sources.default?.uri === currentSource?.uri &&
sources.remote
) {
setCurrentSource(sources.remote)
}
}}
enableLiveTextInteraction={enableLiveTextInteraction}
/>
{blurhashView()}
{dim && theme !== 'light' ? (
<View
style={[
styles.placeholder,
{ backgroundColor: 'black', opacity: theme === 'dark_lighter' ? 0.18 : 0.36 }
]}
style={{
width: '100%',
height: '100%',
position: 'absolute',
backgroundColor: 'black',
opacity: theme === 'dark_lighter' ? 0.18 : 0.36
}}
/>
) : null}
</Pressable>
)
}
const styles = StyleSheet.create({
placeholder: {
width: '100%',
height: '100%',
position: 'absolute'
}
})
export default GracefullyImage

View File

@ -19,12 +19,13 @@ import {
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import * as AuthSession from 'expo-auth-session'
import * as Random from 'expo-random'
import * as Crypto from 'expo-crypto'
import { Image } from 'expo-image'
import * as WebBrowser from 'expo-web-browser'
import { debounce } from 'lodash'
import React, { RefObject, useCallback, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { Alert, Image, KeyboardAvoidingView, Platform, TextInput, View } from 'react-native'
import { Alert, KeyboardAvoidingView, Platform, TextInput, View } from 'react-native'
import { ScrollView } from 'react-native-gesture-handler'
import { fromByteArray } from 'react-native-quick-base64'
import parse from 'url-parse'
@ -78,7 +79,8 @@ const ComponentInstance: React.FC<Props> = ({
clientId,
clientSecret,
scopes: variables.scopes,
redirectUri
redirectUri,
usePKCE: !['pawoo.net'].includes(domain)
})
await request.makeAuthUrlAsync(discovery)
@ -160,7 +162,7 @@ const ComponentInstance: React.FC<Props> = ({
'admin.sign_up': false,
'admin.report': false
},
key: fromByteArray(Random.getRandomBytes(16))
key: fromByteArray(Crypto.getRandomBytes(16))
},
page_local: {
showBoosts: true,
@ -231,7 +233,7 @@ const ComponentInstance: React.FC<Props> = ({
<View style={{ flexDirection: 'row' }}>
<Image
source={require('assets/images/welcome.png')}
style={{ resizeMode: 'contain', flex: 1, aspectRatio: 16 / 9 }}
style={{ flex: 1, aspectRatio: 16 / 9 }}
/>
</View>
) : null}

View File

@ -5,9 +5,9 @@ import { useGlobalStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import { adaptiveScale } from '@utils/styles/scaling'
import { useTheme } from '@utils/styles/ThemeManager'
import { Image } from 'expo-image'
import React from 'react'
import { ColorValue, Platform, TextStyle } from 'react-native'
import FastImage from 'react-native-fast-image'
const regexEmoji = new RegExp(/(:[A-Za-z0-9_]+:)/)
@ -77,8 +77,8 @@ const ParseEmojis: React.FC<Props> = ({
return (
<CustomText key={emojiShortcode + i}>
{i === 0 ? ' ' : undefined}
<FastImage
source={connectMedia({ uri: uri.trim() })}
<Image
source={connectMedia({ uri })}
style={{
width: adaptedFontsize,
height: adaptedFontsize,

View File

@ -79,7 +79,10 @@ const TimelineConversation: React.FC<Props> = ({ conversation, queryKey, highlig
{conversation.accounts.slice(0, 4).map(account => (
<GracefullyImage
key={account.id}
uri={{ original: account.avatar, static: account.avatar_static }}
sources={{
default: { uri: account.avatar },
static: { uri: account.avatar_static }
}}
dimension={{
width: StyleConstants.Avatar.M,
height:

View File

@ -12,11 +12,11 @@ import TimelinePoll from '@components/Timeline/Shared/Poll'
import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { featureCheck } from '@utils/helpers/featureCheck'
import { checkIsMyAccount } from '@utils/helpers/isMyAccount'
import removeHTML from '@utils/helpers/removeHTML'
import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { usePreferencesQuery } from '@utils/queryHooks/preferences'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { useAccountStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { Fragment, useRef, useState } from 'react'
@ -63,10 +63,9 @@ const TimelineDefault: React.FC<Props> = ({
const { colors } = useTheme()
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const [accountId] = useAccountStorage.string('auth.account.id')
const { data: preferences } = usePreferencesQuery()
const ownAccount = status.account?.id === accountId
const isMyAccount = checkIsMyAccount(status.account.id)
const [spoilerExpanded, setSpoilerExpanded] = useState(
preferences?.['reading:expand:spoilers'] || false
)
@ -136,7 +135,7 @@ const TimelineDefault: React.FC<Props> = ({
const mStatus = menuStatus({ status, queryKey })
const mInstance = menuInstance({ status, queryKey })
if (!ownAccount) {
if (!isMyAccount) {
let filterResults: FilteredProps['filterResults'] = []
const [filterRevealed, setFilterRevealed] = useState(false)
const hasFilterServerSide = featureCheck('filter_server_side')
@ -166,7 +165,7 @@ const TimelineDefault: React.FC<Props> = ({
value={{
queryKey,
status,
ownAccount,
isMyAccount,
spoilerHidden,
rawContent,
detectedLanguage,

View File

@ -12,10 +12,10 @@ import TimelinePoll from '@components/Timeline/Shared/Poll'
import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { featureCheck } from '@utils/helpers/featureCheck'
import { checkIsMyAccount } from '@utils/helpers/isMyAccount'
import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { usePreferencesQuery } from '@utils/queryHooks/preferences'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { useAccountStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { Fragment, useState } from 'react'
@ -32,7 +32,6 @@ export interface Props {
}
const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
const [accountId] = useAccountStorage.string('auth.account.id')
const { data: preferences } = usePreferencesQuery()
const status = notification.status?.reblog ? notification.status.reblog : notification.status
@ -42,7 +41,7 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
: notification.status
? notification.status.account
: notification.account
const ownAccount = notification.account?.id === accountId
const isMyAccount = checkIsMyAccount(notification.account?.id)
const [spoilerExpanded, setSpoilerExpanded] = useState(
preferences?.['reading:expand:spoilers'] || false
)
@ -109,7 +108,7 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
const mStatus = menuStatus({ status: notification.status, queryKey })
const mInstance = menuInstance({ status: notification.status, queryKey })
if (!ownAccount) {
if (!isMyAccount) {
let filterResults: FilteredProps['filterResults'] = []
const [filterRevealed, setFilterRevealed] = useState(false)
const hasFilterServerSide = featureCheck('filter_server_side')
@ -140,7 +139,7 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => {
value={{
queryKey,
status,
ownAccount,
isMyAccount,
spoilerHidden
}}
>

View File

@ -22,7 +22,7 @@ import { Pressable, StyleSheet, View } from 'react-native'
import StatusContext from './Context'
const TimelineActions: React.FC = () => {
const { queryKey, status, ownAccount, highlighted, disableDetails } = useContext(StatusContext)
const { queryKey, status, isMyAccount, highlighted, disableDetails } = useContext(StatusContext)
if (!queryKey || !status || disableDetails) return null
const navigationState = useNavState()
@ -182,7 +182,7 @@ const TimelineActions: React.FC = () => {
const childrenReblog = () => {
const color = (state: boolean) => (state ? colors.green : colors.secondary)
const disabled =
status.visibility === 'direct' || (status.visibility === 'private' && !ownAccount)
status.visibility === 'direct' || (status.visibility === 'private' && !isMyAccount)
return (
<>
<Icon
@ -196,7 +196,7 @@ const TimelineActions: React.FC = () => {
fontStyle='S'
style={{
color:
status.visibility === 'private' && !ownAccount
status.visibility === 'private' && !isMyAccount
? colors.disabled
: color(status.reblogged),
marginLeft: StyleConstants.Spacing.XS
@ -258,7 +258,7 @@ const TimelineActions: React.FC = () => {
onPress={onPressReblog}
children={childrenReblog()}
disabled={
status.visibility === 'direct' || (status.visibility === 'private' && !ownAccount)
status.visibility === 'direct' || (status.visibility === 'private' && !isMyAccount)
}
/>

View File

@ -9,7 +9,6 @@ import { StackNavigationProp } from '@react-navigation/stack'
import { RootStackParamList } from '@utils/navigation/navigators'
import { usePreferencesQuery } from '@utils/queryHooks/preferences'
import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation'
import React, { useContext, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable, View } from 'react-native'
@ -207,7 +206,6 @@ const TimelineAttachment = () => {
content={t('shared.attachment.sensitive.button')}
overlay
onPress={() => {
layoutAnimation()
setSensitiveShown(false)
haptics('Light')
}}

View File

@ -7,7 +7,6 @@ import { useTheme } from '@utils/styles/ThemeManager'
import { Audio } from 'expo-av'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { AppState, AppStateStatus, StyleSheet, View } from 'react-native'
import { Blurhash } from 'react-native-blurhash'
import AttachmentAltText from './AltText'
import { aspectRatio } from './dimensions'
@ -72,19 +71,23 @@ const AttachmentAudio: React.FC<Props> = ({ total, index, sensitiveShown, audio
<View style={styles.overlay}>
{sensitiveShown ? (
audio.blurhash ? (
<Blurhash
blurhash={audio.blurhash}
<GracefullyImage
sources={{ blurhash: audio.blurhash }}
style={{
width: '100%',
height: '100%'
}}
dim
/>
) : null
) : (
<>
{audio.preview_url ? (
<GracefullyImage
uri={{ original: audio.preview_url, remote: audio.preview_remote_url }}
sources={{
default: { uri: audio.preview_url },
remote: { uri: audio.preview_remote_url }
}}
style={styles.background}
dim
/>

View File

@ -35,8 +35,11 @@ const AttachmentImage = ({
<GracefullyImage
accessibilityLabel={image.description}
hidden={sensitiveShown}
uri={{ original: image.preview_url, remote: image.remote_url }}
blurhash={image.blurhash}
sources={{
default: { uri: image.preview_url },
remote: { uri: image.remote_url },
blurhash: image.blurhash
}}
onPress={() => navigateToImagesViewer(image.id)}
style={{ aspectRatio: aspectRatio({ total, index, ...image.meta?.original }) }}
dim

View File

@ -1,4 +1,5 @@
import Button from '@components/Button'
import GracefullyImage from '@components/GracefullyImage'
import openLink from '@components/openLink'
import CustomText from '@components/Text'
import { StyleConstants } from '@utils/styles/constants'
@ -6,7 +7,6 @@ import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { View } from 'react-native'
import { Blurhash } from 'react-native-blurhash'
import AttachmentAltText from './AltText'
import { aspectRatio } from './dimensions'
@ -33,8 +33,8 @@ const AttachmentUnsupported: React.FC<Props> = ({ total, index, sensitiveShown,
}}
>
{attachment.blurhash ? (
<Blurhash
blurhash={attachment.blurhash}
<GracefullyImage
sources={{ blurhash: attachment.blurhash }}
style={{
position: 'absolute',
width: '100%',

View File

@ -1,4 +1,5 @@
import Button from '@components/Button'
import GracefullyImage from '@components/GracefullyImage'
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { connectMedia } from '@utils/api/helpers/connect'
import { useAccountStorage, useGlobalStorage } from '@utils/storage/actions'
@ -8,7 +9,6 @@ import { Platform } from 'expo-modules-core'
import * as ScreenOrientation from 'expo-screen-orientation'
import React, { useRef, useState } from 'react'
import { Pressable, View } from 'react-native'
import { Blurhash } from 'react-native-blurhash'
import AttachmentAltText from './AltText'
import { aspectRatio } from './dimensions'
@ -120,7 +120,10 @@ const AttachmentVideo: React.FC<Props> = ({
>
{sensitiveShown ? (
video.blurhash ? (
<Blurhash blurhash={video.blurhash} style={{ width: '100%', height: '100%' }} />
<GracefullyImage
sources={{ blurhash: video.blurhash }}
style={{ width: '100%', height: '100%' }}
/>
) : null
) : !gifv || (gifv && (reduceMotionEnabled || !shouldAutoplayGifv)) ? (
<Button

View File

@ -33,7 +33,10 @@ const TimelineAvatar: React.FC<Props> = ({ account }) => {
onPress={() =>
!disableOnPress && navigation.push('Tab-Shared-Account', { account: actualAccount })
}
uri={{ original: actualAccount.avatar, static: actualAccount.avatar_static }}
sources={{
default: { uri: actualAccount.avatar },
static: { uri: actualAccount.avatar_static }
}}
dimension={
disableDetails || isConversation
? {

View File

@ -82,8 +82,7 @@ const TimelineCard: React.FC = () => {
<>
{status.card?.image ? (
<GracefullyImage
uri={{ original: status.card.image }}
blurhash={status.card.blurhash}
sources={{ default: { uri: status.card.image }, blurhash: status.card.blurhash }}
style={{ flexBasis: StyleConstants.Font.LineHeight.M * 5 }}
imageStyle={{ borderTopLeftRadius: 6, borderBottomLeftRadius: 6 }}
dim

View File

@ -8,7 +8,7 @@ type StatusContextType = {
status?: Mastodon.Status
ownAccount?: boolean
isMyAccount?: boolean
spoilerHidden?: boolean
rawContent?: React.MutableRefObject<string[]> // When highlighted, for translate, edit history
detectedLanguage?: React.MutableRefObject<string>

View File

@ -21,7 +21,7 @@ import { Pressable, View } from 'react-native'
import StatusContext from './Context'
const TimelinePoll: React.FC = () => {
const { queryKey, status, ownAccount, spoilerHidden, disableDetails, highlighted } =
const { queryKey, status, isMyAccount, spoilerHidden, disableDetails, highlighted } =
useContext(StatusContext)
if (!queryKey || !status || !status.poll) return null
const poll = status.poll
@ -72,7 +72,7 @@ const TimelinePoll: React.FC = () => {
const pollButton = () => {
if (!poll.expired) {
if (!ownAccount && !poll.voted) {
if (!isMyAccount && !poll.voted) {
return (
<View style={{ marginRight: StyleConstants.Spacing.S }}>
<Button

View File

@ -60,6 +60,9 @@ const Timeline: React.FC<Props> = ({
const { colors, theme } = useTheme()
const { t } = useTranslation('componentTimeline')
const firstLoad = useSharedValue<boolean>(!readMarker || disableRefresh)
const shouldAutoFetch = useSharedValue<boolean>(!!readMarker && !disableRefresh)
const { data, refetch, isFetching, isLoading, isRefetching, fetchNextPage, isFetchingNextPage } =
useTimelineQuery({
...queryKey[1],
@ -68,7 +71,13 @@ const Timeline: React.FC<Props> = ({
notifyOnChangeProps: Platform.select({
ios: ['dataUpdatedAt', 'isFetching'],
android: ['dataUpdatedAt', 'isFetching', 'isLoading']
})
}),
onSuccess: () => {
if (!firstLoad.value) {
firstLoad.value = true
fetchingType.value = 1
}
}
}
})
@ -90,6 +99,9 @@ const Timeline: React.FC<Props> = ({
(curr, prev) => {
if (curr !== null && prev === null) {
notifiedFetchedNotice.value = false
if (curr === 0) {
shouldAutoFetch.value = false
}
}
},
[fetchedCount]
@ -129,19 +141,41 @@ const Timeline: React.FC<Props> = ({
fetchingType.value = 2
} else if (y <= SEPARATION_Y_1) {
fetchingType.value = 1
shouldAutoFetch.value = true
}
}
}
},
[isFetching]
)
useAnimatedReaction(
() => scrollY.value < 600,
(curr, prev) => {
if (
curr === true &&
prev === false &&
!isFetchingPrev.value &&
fetchingType.value === 0 &&
shouldAutoFetch.value &&
Platform.OS === 'ios'
) {
fetchingType.value = 1
}
}
)
const latestMarker = useRef<string>()
const updateMarkers = useCallback(
throttle(
() => readMarker && setAccountStorage([{ key: readMarker, value: latestMarker.current }]),
1000 * 15
),
throttle(() => {
if (readMarker) {
const currentMarker = getAccountStorage.string(readMarker) || '0'
if ((latestMarker.current || '0') > currentMarker) {
setAccountStorage([{ key: readMarker, value: latestMarker.current }])
} else {
// setAccountStorage([{ key: readMarker, value: '105250709762254246' }])
}
}
}, 1000 * 15),
[]
)
readMarker &&
@ -159,24 +193,14 @@ const Timeline: React.FC<Props> = ({
{
viewabilityConfig: {
minimumViewTime: 300,
itemVisiblePercentThreshold: 80,
waitForInteraction: true
itemVisiblePercentThreshold: 10,
waitForInteraction: false
},
onViewableItemsChanged: ({ viewableItems }) => {
const marker = readMarker ? getAccountStorage.string(readMarker) : undefined
const firstItemId = viewableItems.filter(item => item.isViewable)[0]?.item.id
if (
!isFetchingPrev.value &&
!isRefetching &&
firstItemId &&
firstItemId > (marker || '0')
) {
if (!isFetchingPrev.value && !isRefetching && firstItemId) {
latestMarker.current = firstItemId
updateMarkers()
} else {
// latestMarker.current = '105250709762254246'
// updateMarkers()
}
}
}

View File

@ -4,6 +4,7 @@ import { useNavigation } from '@react-navigation/native'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { useQueryClient } from '@tanstack/react-query'
import apiInstance from '@utils/api/instance'
import { checkIsMyAccount } from '@utils/helpers/isMyAccount'
import { TabSharedStackParamList, useNavState } from '@utils/navigation/navigators'
import { useAccountQuery } from '@utils/queryHooks/account'
import {
@ -15,7 +16,7 @@ import {
MutationVarsTimelineUpdateAccountProperty,
useTimelineMutation
} from '@utils/queryHooks/timeline'
import { getAccountStorage, getReadableAccounts, useAccountStorage } from '@utils/storage/actions'
import { getAccountStorage, getReadableAccounts } from '@utils/storage/actions'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Alert, Platform } from 'react-native'
@ -40,19 +41,18 @@ const menuAccount = ({
const [enabled, setEnabled] = useState(openChange)
useEffect(() => {
if (!ownAccount && enabled === false && openChange === true) {
if (!isMyAccount && enabled === false && openChange === true) {
setEnabled(true)
}
}, [openChange, enabled])
const { data: fetchedAccount } = useAccountQuery({ account, _local: true, options: { enabled } })
const actualAccount = status?._remote ? fetchedAccount : account
const isMyAccount = checkIsMyAccount(actualAccount?.id)
const { data, isFetched } = useRelationshipQuery({
id: actualAccount?.id,
options: { enabled: !!actualAccount?.id && enabled }
})
const ownAccount = useAccountStorage.string('auth.account.id')['0'] === actualAccount?.id
const queryClient = useQueryClient()
const timelineMutation = useTimelineMutation({
onSuccess: (_, params) => {
@ -134,7 +134,7 @@ const menuAccount = ({
if (!account) return []
if (!ownAccount && Platform.OS !== 'android' && type !== 'account') {
if (!isMyAccount && Platform.OS !== 'android' && type !== 'account') {
menus[0].push({
type: 'item',
key: 'account-following',
@ -165,7 +165,7 @@ const menuAccount = ({
})
}
if (!ownAccount) {
if (!isMyAccount) {
menus[0].push({
type: 'item',
key: 'account-list',

View File

@ -4,6 +4,7 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { useQueryClient } from '@tanstack/react-query'
import apiInstance from '@utils/api/instance'
import { featureCheck } from '@utils/helpers/featureCheck'
import { checkIsMyAccount } from '@utils/helpers/isMyAccount'
import { RootStackParamList, useNavState } from '@utils/navigation/navigators'
import {
MutationVarsTimelineUpdateStatusProperty,
@ -57,9 +58,8 @@ const menuStatus = ({
const menus: ContextMenu = []
const [accountId] = useAccountStorage.string('auth.account.id')
const [accountAcct] = useAccountStorage.string('auth.account.acct')
const ownAccount = accountId === status.account?.id
const isMyAccount = checkIsMyAccount(status.account.id)
const canEditPost = featureCheck('edit_post')
@ -98,7 +98,7 @@ const menuStatus = ({
},
disabled: false,
destructive: false,
hidden: !ownAccount || !canEditPost
hidden: !isMyAccount || !canEditPost
},
title: t('componentContextMenu:status.edit.action'),
icon: 'square.and.pencil'
@ -142,7 +142,7 @@ const menuStatus = ({
),
disabled: false,
destructive: true,
hidden: !ownAccount
hidden: !isMyAccount
},
title: t('componentContextMenu:status.deleteEdit.action'),
icon: 'pencil.and.outline'
@ -171,7 +171,7 @@ const menuStatus = ({
),
disabled: false,
destructive: true,
hidden: !ownAccount
hidden: !isMyAccount
},
title: t('componentContextMenu:status.delete.action'),
icon: 'trash'
@ -195,7 +195,7 @@ const menuStatus = ({
disabled: false,
destructive: false,
hidden:
!ownAccount &&
!isMyAccount &&
queryKey[1].page !== 'Notifications' &&
!status.mentions?.find(
mention => mention.acct === accountAcct && mention.username === accountAcct
@ -224,7 +224,7 @@ const menuStatus = ({
}),
disabled: status.visibility !== 'public' && status.visibility !== 'unlisted',
destructive: false,
hidden: !ownAccount
hidden: !isMyAccount
},
title: t('componentContextMenu:status.pin.action', {
defaultValue: 'false',
@ -236,8 +236,9 @@ const menuStatus = ({
type: 'item',
key: 'status-filter',
props: {
// @ts-ignore
onSelect: () => navigation.navigate('Tab-Shared-Filter', { source: 'status', status, queryKey }),
onSelect: () =>
// @ts-ignore
navigation.navigate('Tab-Shared-Filter', { source: 'status', status, queryKey }),
disabled: false,
destructive: false,
hidden: !('filtered' in status)

View File

@ -46,17 +46,9 @@ const openLink = async (url: string, navigation?: any) => {
// If an account can be found
if (match?.account) {
if (!match.account._remote && match.account.id) {
handleNavigation('Tab-Shared-Account', { account: match.account.id })
return
}
let response: Mastodon.Account | undefined = undefined
const queryKey: QueryKeyAccount = [
'Account',
{ id: match.account.id, url: url, _remote: match.account._remote }
]
const queryKey: QueryKeyAccount = ['Account', { url: url, _remote: match.account._remote }]
const cache = queryClient.getQueryData<Mastodon.Status>(queryKey)
if (cache) {

View File

@ -33,7 +33,7 @@
"poll": "S'ha acabat una enquesta en què havies participat",
"reblog": {
"default": "{{name}} ha impulsat",
"myself": "",
"myself": "He impulsat",
"notification": "{{name}} ha impulsat la teva publicació"
},
"update": "L'impuls ha sigut editat",

View File

@ -33,7 +33,7 @@
"poll": "Eine Umfrage, an der du teilgenommen hast, ist beendet",
"reblog": {
"default": "{{name}} hat geboostet",
"myself": "",
"myself": "Von mir geboosted",
"notification": "{{name}} hat deinen Tröt geboostet"
},
"update": "Boost wurde bearbeitet",

View File

@ -33,7 +33,7 @@
"poll": "Una encuesta en la que has votado ha terminado",
"reblog": {
"default": "{{name}} ha impulsado",
"myself": "",
"myself": "He impulsado",
"notification": "{{name}} ha impulsado tu toot"
},
"update": "El impulso ha sido editado",

View File

@ -33,7 +33,7 @@
"poll": "Erantzun zenuen inkesta bat amaitu da",
"reblog": {
"default": "{{name}}-(e)k bultzatu du",
"myself": "",
"myself": "Bultzatu dut",
"notification": "{{name}}-(e)k zure tuta bultzatu du"
},
"update": "Bultzada editatua izan da",

View File

@ -19,8 +19,8 @@
"refetch": "更新",
"fetching": "新しいトゥートを取得しています...",
"fetched": {
"none": "",
"found": ""
"none": "新しいトゥートはありません",
"found": "{{count}}トゥートを取得"
}
},
"shared": {
@ -33,7 +33,7 @@
"poll": "アンケートが終了しました",
"reblog": {
"default": "{{name}}さんがブースト",
"myself": "",
"myself": "ブーストしました",
"notification": "{{name}}さんがあなたのトゥートをブーストしました"
},
"update": "ブーストしたトゥートが編集されました",

View File

@ -73,13 +73,13 @@
"name": "設定"
},
"preferencesFilters": {
"name": ""
"name": "すべてのコンテンツフィルター"
},
"preferencesFilterAdd": {
"name": "フィルターを作成"
},
"preferencesFilterEdit": {
"name": ""
"name": "フィルターを編集"
},
"profile": {
"name": "プロフィールを編集"
@ -136,7 +136,7 @@
},
"preferences": {
"visibility": {
"title": "",
"title": "デフォルトの投稿の表示",
"options": {
"public": "公開",
"unlisted": "未収載",
@ -144,40 +144,40 @@
}
},
"sensitive": {
"title": ""
"title": "メディアを常に閲覧注意としてマークする"
},
"media": {
"title": "",
"title": "メディアの表示",
"options": {
"default": "",
"show_all": "",
"hide_all": ""
"default": "閲覧注意としてマークされたメディアを隠す",
"show_all": "メディアを常に表示する",
"hide_all": "メディアを常に隠す"
}
},
"spoilers": {
"title": ""
"title": "閲覧注意としてマークされたトゥートを常に展開する"
},
"autoplay_gifs": {
"title": ""
"title": "トゥートでGIFを自動再生"
},
"filters": {
"title": "",
"content": ""
"title": "コンテンツフィルター",
"content": "{{count}} がアクティブです"
},
"web_only": {
"title": "",
"description": ""
"title": "アップデート設定",
"description": "以下の設定は、web UI を使用してのみ更新できます"
}
},
"preferencesFilters": {
"expired": "期限切れ",
"keywords_one": "",
"keywords_other": "",
"statuses_one": "",
"statuses_other": "",
"context": "",
"keywords_one": "{{count}}件のキーワード",
"keywords_other": "{{count}}件のキーワード",
"statuses_one": "{{count}}トゥート",
"statuses_other": "{{count}}トゥート",
"context": "<0 />で適用",
"contexts": {
"home": "",
"home": "フォロー中とリスト",
"notifications": "通知",
"public": "連合",
"thread": "会話",
@ -196,22 +196,22 @@
"604800": "1週間後",
"18144000": "1ヶ月後"
},
"context": "",
"context": "適用:",
"contexts": {
"home": "",
"home": "フォロー中とリスト",
"notifications": "通知",
"public": "連合タイムライン",
"thread": "",
"account": ""
"thread": "会話表示",
"account": "プロフィール表示"
},
"action": "",
"action": "一致した時",
"actions": {
"warn": "",
"hide": ""
"hide": "完全に非表示にする"
},
"keywords": "",
"keywords": "これらのキーワードと一致",
"keyword": "キーワード",
"statuses": ""
"statuses": "これらのトゥートと一致"
},
"profile": {
"feedback": {
@ -401,7 +401,7 @@
},
"filter": {
"name": "フィルターに追加",
"existed": ""
"existed": "フィルター内に存在"
},
"history": {
"name": "編集履歴"

View File

@ -6,7 +6,7 @@
"action_false": "사용자 팔로우",
"action_true": "사용자 팔로우 해제"
},
"inLists": "",
"inLists": "사용자를 포함한 리스트 ...",
"showBoosts": {
"action_false": "사용자의 부스트 보이기",
"action_true": "사용자의 부스트 숨기기"
@ -16,12 +16,12 @@
"action_true": "사용자 뮤트 해제"
},
"followAs": {
"trigger": "",
"trigger": "특정 계정에서 팔로우 ...",
"succeed_default": "@{{source}} 계정에서 @{{target}} 계정을 팔로우 했어요.",
"succeed_locked": "@{{source}} 계정에서 @{{target}} 계정의 팔로우 승인을 요청했어요.",
"failed": "특정 계정에서 팔로우"
},
"blockReport": "",
"blockReport": "차단 및 신고",
"block": {
"action_false": "사용자 차단",
"action_true": "사용자 차단 해제",
@ -56,11 +56,11 @@
},
"hashtag": {
"follow": {
"action_false": "",
"action_true": ""
"action_false": "팔로우",
"action_true": "팔로우 해제"
},
"filter": {
"action": ""
"action": "해시태그 필터 ..."
}
},
"share": {
@ -99,8 +99,8 @@
"action_true": "툿 고정 해제"
},
"filter": {
"action_false": "",
"action_true": ""
"action_false": "툿 필터 ...",
"action_true": "필터 관리 ..."
}
}
}

View File

@ -17,10 +17,10 @@
"refresh": {
"fetchPreviousPage": "이 시점에 이어서 불러오기",
"refetch": "최신 내용 불러오기",
"fetching": "",
"fetching": "새로운 툿 불러오는 중 ...",
"fetched": {
"none": "",
"found": ""
"none": "새로운 툿이 없어요",
"found": "새로운 툿 {{count}}개를 불러왔어요"
}
},
"shared": {
@ -33,7 +33,7 @@
"poll": "내가 참여한 투표가 끝났어요",
"reblog": {
"default": "{{name}} 님이 부스트했어요",
"myself": "",
"myself": "내가 부스트함",
"notification": "{{name}} 님이 내 툿을 부스트했어요"
},
"update": "부스트한 툿이 수정됨",

View File

@ -5,7 +5,7 @@
"title": "작성을 취소할까요?",
"buttons": {
"save": "임시 저장",
"delete": "임시 저장한 내용 삭제"
"delete": "작성중인 내용 삭제"
}
}
},

View File

@ -70,16 +70,16 @@
"name": "푸시 알림"
},
"preferences": {
"name": ""
"name": "설정"
},
"preferencesFilters": {
"name": ""
"name": "콘텐츠 필터"
},
"preferencesFilterAdd": {
"name": ""
"name": "필터 만들기"
},
"preferencesFilterEdit": {
"name": ""
"name": "필터 편집"
},
"profile": {
"name": "프로필 편집"
@ -136,81 +136,81 @@
},
"preferences": {
"visibility": {
"title": "",
"title": "공개 범위 기본값",
"options": {
"public": "",
"unlisted": "",
"private": ""
"public": "공개",
"unlisted": "공개 타임라인에 비표시",
"private": "팔로워만"
}
},
"sensitive": {
"title": ""
"title": "민감한 미디어 표시를 기본값으로 사용"
},
"media": {
"title": "",
"title": "미디어 표시",
"options": {
"default": "",
"show_all": "",
"hide_all": ""
"default": "민감한 미디어 숨기기",
"show_all": "모든 미디어 표시",
"hide_all": "모든 미디어 숨기기"
}
},
"spoilers": {
"title": ""
"title": "스포일러 경고가 있는 툿 자동으로 펼치기"
},
"autoplay_gifs": {
"title": ""
"title": "툿에 포함된 GIF 자동 재생"
},
"filters": {
"title": "",
"content": ""
"title": "콘텐츠 필터",
"content": "{{count}}개 활성화"
},
"web_only": {
"title": "",
"description": ""
"title": "추가 설정",
"description": "일부 설정은 웹 UI에서만 변경할 수 있어요"
}
},
"preferencesFilters": {
"expired": "",
"keywords_one": "",
"keywords_other": "",
"statuses_one": "",
"statuses_other": "",
"context": "",
"expired": "만료됨",
"keywords_one": "{{count}}개의 단어",
"keywords_other": "{{count}}개의 단어",
"statuses_one": "{{count}}개의 툿",
"statuses_other": "{{count}}개의 툿",
"context": "<0 />에 적용됨",
"contexts": {
"home": "",
"notifications": "",
"public": "",
"thread": "",
"account": ""
"home": "팔로우와 리스트",
"notifications": "알림",
"public": "연합",
"thread": "대화",
"account": "프로필"
}
},
"preferencesFilter": {
"name": "",
"expiration": "",
"name": "이름",
"expiration": "만료일",
"expirationOptions": {
"0": "",
"1800": "",
"3600": "",
"43200": "",
"86400": "",
"604800": "",
"18144000": ""
"0": "만료하지 않음",
"1800": "30분 후",
"3600": "1시간 후",
"43200": "12시간 후",
"86400": "1일 후",
"604800": "1주일 후",
"18144000": "1개월 후"
},
"context": "",
"context": "적용할 대상",
"contexts": {
"home": "",
"notifications": "",
"public": "",
"thread": "",
"account": ""
"home": "팔로우와 리스트",
"notifications": "알림",
"public": "연합 타임라인",
"thread": "대화 내용",
"account": "프로필 내용"
},
"action": "",
"action": "필터 동작",
"actions": {
"warn": "",
"hide": ""
"warn": "내용만 가리고 펼쳐볼 수 있게 하기",
"hide": "완전히 숨기기"
},
"keywords": "",
"keyword": "",
"keywords": "필터할 단어",
"keyword": "단어",
"statuses": ""
},
"profile": {
@ -400,7 +400,7 @@
"name": "<0 /><1>의 미디어</1>"
},
"filter": {
"name": "",
"name": "필터에 추가",
"existed": ""
},
"history": {

32
src/i18n/no/common.json Normal file
View File

@ -0,0 +1,32 @@
{
"buttons": {
"OK": "OK",
"apply": "Bruk",
"cancel": "Avbryt",
"discard": "Forkast",
"continue": "Fortsett",
"create": "Opprett",
"delete": "Slett",
"done": "Fullført",
"confirm": "Bekreft"
},
"customEmoji": {
"accessibilityLabel": "Tilpasset emoji {{emoji}}"
},
"message": {
"success": {
"message": "{{function}} var vellykket"
},
"warning": {
"message": ""
},
"error": {
"message": "{{function}} feilet, vennligst prøv igjen"
}
},
"separator": ", ",
"discard": {
"title": "Endringene er ikke lagret",
"message": "Dine endringer er ikke lagret. Angrer du på endringene?"
}
}

View File

@ -0,0 +1,106 @@
{
"accessibilityHint": "Handlinger for tut, for eksempel dens bruker, tut selv",
"account": {
"title": "Brukerhandlinger",
"following": {
"action_false": "Følg bruker",
"action_true": "Slutt å følge"
},
"inLists": "Lister som inneholder bruker ...",
"showBoosts": {
"action_false": "Vis brukerens booster",
"action_true": "Skjul brukernes booster"
},
"mute": {
"action_false": "Demp bruker",
"action_true": "Opphev demping av bruker"
},
"followAs": {
"trigger": "Følg som ...",
"succeed_default": "Følger nå @{{target}} med @{{source}}",
"succeed_locked": "Sendt følge forespørsel til @{{target}} med {{source}}, venter på godkjenning",
"failed": "Følg som"
},
"blockReport": "Blokker og rapporter",
"block": {
"action_false": "Blokker bruker",
"action_true": "Fjern blokkering av brukker",
"alert": {
"title": "Bekreft blokkering av @{{username}}?"
}
},
"reports": {
"action": "Rapporter og blokker bruker",
"alert": {
"title": "Bekreft blokkering av @{{username}}?"
}
}
},
"at": {
"direct": "Direktemelding",
"public": "Offentlig melding"
},
"copy": {
"action": "Kopier tut",
"succeed": "Kopiert"
},
"instance": {
"title": "Instansaktivitet",
"block": {
"action": "Blokker instans {{instance}}",
"alert": {
"title": "Bekreft blokkering av @{{instance}}?",
"message": "For det meste kan du dempe eller blokkere visse brukere.\n\nEtter at du har blokkert, vil alt innholdet fra denne forekomsten bli fjernet!"
}
}
},
"hashtag": {
"follow": {
"action_false": "Følg",
"action_true": "Slutt å følge"
},
"filter": {
"action": "Filtrer emnet ..."
}
},
"share": {
"status": {
"action": "Del tut"
},
"account": {
"action": "Del bruker"
}
},
"status": {
"title": "Tut handlinger",
"edit": {
"action": "Rediger tut"
},
"delete": {
"action": "Slett tut",
"alert": {
"title": "Bekreft sletting?",
"message": "Alle booster og favoritter blir slettet, inkludert alle svar."
}
},
"deleteEdit": {
"action": "Slett tut og repost",
"alert": {
"title": "Bekreft sletting og repost?",
"message": "Alle booster og favoritter blir slettet, inkludert alle svar."
}
},
"mute": {
"action_false": "Demp tut og svar",
"action_true": "Avdemp tut og svar"
},
"pin": {
"action_false": "Fest tut",
"action_true": "Avfest tut"
},
"filter": {
"action_false": "Filtrer tut...",
"action_true": "Behandle filtere..."
}
}
}

View File

@ -0,0 +1,3 @@
{
"frequentUsed": "Ofte brukt"
}

View File

@ -0,0 +1,27 @@
{
"server": {
"textInput": {
"placeholder": "Instansens domene"
},
"whitelisted": "Dette kan være en hvitelistet forekomst som tooot ikke kan hente data fra før du logger inn.",
"button": "Logg inn",
"information": {
"name": "Navn",
"accounts": "Brukere",
"statuses": "Tuter",
"domains": "Universum"
},
"disclaimer": {
"base": "Innlogging bruker systemnettleser mens du logger inn på, din kontoinformasjon vil ikke være synlig for toot-appen."
},
"terms": {
"base": "Ved å logge inn godtar du <0>retningslinjer for personvern</0> og <1>vilkårene for bruk</1>."
}
},
"update": {
"alert": {
"title": "Logget på denne instansen",
"message": "Du kan logge inn på en annen konto, og beholde eksisterende pålogging"
}
}
}

View File

@ -0,0 +1,10 @@
{
"title": "Velg mediekilde",
"message": "Media EXIF-data er ikke lastet opp",
"options": {
"image": "Last opp bilder",
"image_max": "Last opp bilder (maks {{max}})",
"video": "Last opp video",
"video_max": "Last opp video (maks {{max}})"
}
}

View File

@ -0,0 +1,8 @@
{
"HTML": {
"accessibilityHint": "Trykk for å utvide eller skjule innhold",
"expanded": "{{hint}}{{moreLines}}",
"moreLines": " ({{count}} flere linjer)",
"defaultHint": "Lang tut"
}
}

View File

@ -0,0 +1,16 @@
{
"follow": {
"function": "Følg bruker"
},
"block": {
"function": "Blokker bruker"
},
"button": {
"error": "Feil med lasting",
"blocked_by": "Blokkert av bruker",
"blocking": "Fjern blokkering",
"following": "Slutt å følge",
"requested": "Avbryt forespørsel",
"default": "Følg"
}
}

View File

@ -0,0 +1,168 @@
{
"empty": {
"error": {
"message": "Lasting mislyktes",
"button": "Prøv igjen"
},
"success": {
"message": "Tidslinjen er tom"
}
},
"end": {
"message": "Slutt, hva med en kopp med <0 />"
},
"lookback": {
"message": "Sist lest"
},
"refresh": {
"fetchPreviousPage": "Nyere herfra",
"refetch": "Til nyeste",
"fetching": "Henter nyere tuter ...",
"fetched": {
"none": "Ingen nyere tut",
"found": "Hentet {{count}} tuter"
}
},
"shared": {
"actioned": {
"pinned": "Festet",
"favourite": "{{name}} favoriserte din tut",
"status": "{name} la nettopp ut",
"follow": "{{name}} følger deg",
"follow_request": "{{name}} ba om å følge deg",
"poll": "En avstemming du har stemt på er avsluttet",
"reblog": {
"default": "{{name}} boostet",
"myself": "Jeg boostet",
"notification": "{{name}} boostet ditt tut"
},
"update": "Reblogg har blitt redigert",
"admin.sign_up": "{{name}} ble med i instansen",
"admin.report": "{{name}} rapportert:"
},
"actions": {
"reply": {
"accessibilityLabel": "Svar på tut"
},
"reblogged": {
"accessibilityLabel": "Boost denne tut",
"function": "Boost tut",
"options": {
"title": "Velg synlighet for boost",
"public": "Offentlig boost",
"unlisted": "Fjern boost"
}
},
"favourited": {
"accessibilityLabel": "Legge tut til dine favoritter",
"function": "Favoritt tut"
},
"bookmarked": {
"accessibilityLabel": "Legg tut til bokmerker",
"function": "Bokmerk tut"
},
"openReport": "Åpne rapport"
},
"actionsUsers": {
"reblogged_by": {
"accessibilityLabel": "{{count}} brukere har boostet tuten",
"accessibilityHint": "Trykk for å bli kjent med brukerne",
"text": "$t(screenTabs:shared.users.statuses.reblogged_by)"
},
"favourited_by": {
"accessibilityLabel": "{{count}} brukere har boostet tuten",
"accessibilityHint": "Trykk for å bli kjent med brukerne",
"text": "$t(screenTabs:shared.users.statuses.favourited_by)"
},
"history": {
"accessibilityLabel": "Tut har blitt redigert {{count}} ganger",
"accessibilityHint": "Trykk for å vise hele redigeringsloggen",
"text_one": "{{count}} rediger",
"text_other": "{{count}} redigeringer"
}
},
"attachment": {
"sensitive": {
"button": "Vis sensitivt media"
},
"unsupported": {
"text": "Feil med lasting",
"button": "Prøv ekstern lenke"
},
"altText": "Alternativ tekst"
},
"avatar": {
"accessibilityLabel": "Profilbilde av {{name}}",
"accessibilityHint": "Trykk for å gå til {{name}} sin side"
},
"content": {
"expandHint": "Skjult innhold"
},
"filtered": {
"reveal": "Vis likevel",
"match_v1": "Filtrert: {{phrase}}.",
"match_v2_one": "Filtrert av {{filters}}.",
"match_v2_other": "Filtrert med {{count}} filtre, {{filters}}."
},
"fullConversation": "Vis samtaler",
"translate": {
"default": "Oversett",
"succeed": "Oversatt med {{provider}} fra {{source}}",
"failed": "Oversettelse feilet",
"source_not_supported": "dette språket er ikke støttet",
"target_not_supported": "Dette språket er ikke støttet"
},
"header": {
"shared": {
"account": {
"name": {
"accessibilityHint": "Brukerens visningsnavn"
},
"account": {
"accessibilityHint": "Brukerkonto"
}
},
"application": "med {{application}}",
"edited": {
"accessibilityLabel": "Tut redigert"
},
"muted": {
"accessibilityLabel": "Tut er dempet"
},
"replies": "Svar <0 />",
"visibility": {
"direct": {
"accessibilityLabel": "Tut er en direkte melding"
},
"private": {
"accessibilityLabel": "Tut er kun synlig for følgere"
}
}
},
"conversation": {
"withAccounts": "Med",
"delete": {
"function": "Slett direkte melding"
}
}
},
"poll": {
"meta": {
"button": {
"vote": "Stem",
"refresh": "Oppdater"
},
"count": {
"voters_one": "{{count}} bruker har stemt",
"voters_other": "{{count}} bruker har stemt",
"votes_one": "%{count} stemme",
"votes_other": "%{count} stemmer"
},
"expiration": {
"expired": "Avstemning utløpt",
"until": "Utløper <0 />"
}
}
}
}
}

17
src/i18n/no/screens.json Normal file
View File

@ -0,0 +1,17 @@
{
"screenshot": {
"title": "Personvernbeskyttelse",
"message": "Vennligst ikke avslør brukerens identitet, som f. eks. brukernavn, profilbilde, osv. Tusen takk!"
},
"localCorrupt": {
"message": "Din økt er utløpt, vennligst logg inn igjen"
},
"pushError": {
"message": "Feil ved push-tjeneste",
"description": "Vennligst aktiver push-varsling i innstillingene"
},
"shareError": {
"imageNotSupported": "Bildetype {{type}} støttes ikke",
"videoNotSupported": "Videotype {{type}} støttes ikke"
}
}

View File

@ -0,0 +1,6 @@
{
"heading": "Del tut...",
"content": {
"select_account": "Velg konto"
}
}

View File

@ -0,0 +1,10 @@
{
"heading": "Kunngjøringer",
"content": {
"published": "Publisert <0 />",
"button": {
"read": "Les",
"unread": "Merk som lest"
}
}
}

View File

@ -0,0 +1,171 @@
{
"heading": {
"left": {
"alert": {
"title": "Avbryte redigering?",
"buttons": {
"save": "Lagre utkast",
"delete": "Slett utkast"
}
}
},
"right": {
"button": {
"default": "Tut",
"conversation": "Tut DM",
"reply": "Tut svar",
"deleteEdit": "Tut",
"edit": "Tut",
"share": "Tut"
},
"alert": {
"default": {
"title": "Tut mislyktes",
"button": "Prøv igjen"
},
"removeReply": {
"title": "Svar på tut ble ikke funnet",
"description": "Svarte tut kan ha blitt slettet. Vil du fjerne det fra din referanse?",
"confirm": "Fjern referanse"
}
}
}
},
"content": {
"root": {
"header": {
"postingAs": "Tuter som @{{acct}}@{{domain}}",
"spoilerInput": {
"placeholder": "Spoiler advarsel"
},
"textInput": {
"placeholder": "Hva tenker du på",
"keyboardImage": {
"exceedMaximum": {
"title": "Maksimalt antall vedlegg nådd"
}
}
}
},
"footer": {
"attachments": {
"sensitive": "Merk vedlegg som følsomt",
"remove": {
"accessibilityLabel": "Fjern opplastet vedlegg, nummer {{attachment}}"
},
"edit": {
"accessibilityLabel": "Rediger opplastet vedlegg, nummer {{attachment}}"
},
"upload": {
"accessibilityLabel": "Last opp flere vedlegg"
}
},
"emojis": {
"accessibilityHint": "Trykk for å legge til emoji"
},
"poll": {
"option": {
"placeholder": {
"accessibilityLabel": "Avstemningsvalg {{index}}",
"single": "Ett valg",
"multiple": "Flere valg"
}
},
"quantity": {
"reduce": {
"accessibilityLabel": "Reduser valg til {{amount}}",
"accessibilityHint": "Minimum antall valg nådd, har {{amount}}"
},
"increase": {
"accessibilityLabel": "Reduser valg til {{amount}}",
"accessibilityHint": "Maksimum antall valg nådd, har {{amount}}"
}
},
"multiple": {
"heading": "Valg type",
"options": {
"single": "Ett valg",
"multiple": "Flere valg"
}
},
"expiration": {
"heading": "Gyldighet",
"options": {
"300": "5 minutter",
"1800": "30 minutter",
"3600": "1 time",
"21600": "6 timer",
"86400": "1 dag",
"259200": "3 dager",
"604800": "7 dager"
}
}
}
},
"actions": {
"attachment": {
"accessibilityLabel": "Last opp vedlegg",
"accessibilityHint": "Avstemmingsfunksjonen blir deaktivert når der er noen vedlegg",
"failed": {
"alert": {
"title": "Opplasting feilet",
"button": "Prøv igjen"
}
}
},
"poll": {
"accessibilityLabel": "Legg til avstemning",
"accessibilityHint": "Vedlegg vil bli deaktivert når avstemningen er aktiv"
},
"visibility": {
"accessibilityLabel": "Tut-synlighet er {{visibility}}",
"title": "Tut-synlighet",
"options": {
"public": "Offentlig",
"unlisted": "Uoppført",
"private": "Kun følgere",
"direct": "Direktemelding"
}
},
"spoiler": {
"accessibilityLabel": "Spoiler"
},
"emoji": {
"accessibilityLabel": "Legg til emoji",
"accessibilityHint": "Åpne emoji-valgpanel, sveip horisontalt for å endre side"
}
},
"drafts_one": "Utkast ({{count}})",
"drafts_other": "Utkast ({{count}})"
},
"editAttachment": {
"header": {
"title": "Rediger vedlegg",
"right": {
"accessibilityLabel": "Lagre redigering av vedlegg",
"failed": {
"title": "Redigering mislyktes",
"button": "Prøv igjen"
}
}
},
"content": {
"altText": {
"heading": "Beskriv media for synshemmede",
"placeholder": "Du kan legge til en beskrivelse, noen ganger kalt alt-tekst, på dine medier slik at de er tilgjengelige for enda flere mennesker, også for dem som er blinde eller svaksynte.\n\nGode beskrivelser er konkret, men presenterer hva som er i dine medier nøyaktig nok til å forstå konteksten."
}
}
},
"draftsList": {
"header": {
"title": "Utkast"
},
"warning": "Utkast lagres bare lokalt, og kan ved uhell gå tapt. Bruk ikke utkast til langtidslagring.",
"content": {
"accessibilityHint": "Lagret utkast, trykk for å redigere dette utkastet",
"textEmpty": "Innhold er tomt"
},
"checkAttachment": "Sjekker vedlegg på serveren..."
}
}
}

View File

@ -0,0 +1,16 @@
{
"content": {
"actions": {
"accessibilityLabel": "Flere handlinger med dette bildet",
"accessibilityHint": "Du kan lagre eller dele dette bildet"
},
"options": {
"save": "Lagre bilde",
"share": "Del bilde"
},
"save": {
"succeed": "Bilde lagret",
"failed": "Lagring av bilde feilet"
}
}
}

View File

@ -0,0 +1,475 @@
{
"tabs": {
"local": {
"name": "Følger",
"options": {
"showBoosts": "Vis booster",
"showReplies": "Vis svar"
}
},
"public": {
"segments": {
"federated": "Føderert",
"local": "Lokal",
"trending": "Populært"
}
},
"notifications": {
"name": "Varsler"
}
},
"common": {
"search": {
"accessibilityLabel": "Søk",
"accessibilityHint": "Søk etter emneknagger, brukere eller tuter"
}
},
"notifications": {
"filters": {
"accessibilityLabel": "Filter",
"accessibilityHint": "Filtrer viste varslingstyper",
"title": "Vis varsler"
}
},
"me": {
"stacks": {
"bookmarks": {
"name": "Bokmerker"
},
"conversations": {
"name": "Direktemeldinger"
},
"favourites": {
"name": "Favoritter"
},
"followedTags": {
"name": "Fulgte emneknagger"
},
"fontSize": {
"name": "Skriftstørrelse for tut"
},
"language": {
"name": "Språk"
},
"list": {
"name": "Liste: {{list}}"
},
"listAccounts": {
"name": "Brukere i listen: {{list}}"
},
"listAdd": {
"name": "Opprett liste"
},
"listEdit": {
"name": "Rediger detaljer"
},
"lists": {
"name": "Lister"
},
"push": {
"name": "Pushvarsling"
},
"preferences": {
"name": "Brukervalg"
},
"preferencesFilters": {
"name": "Alle innholdsfiltre"
},
"preferencesFilterAdd": {
"name": "Opprett Filter"
},
"preferencesFilterEdit": {
"name": "Rediger Filter"
},
"profile": {
"name": "Rediger Profil"
},
"profileName": {
"name": "Rediger visningsnavn"
},
"profileNote": {
"name": "Rediger beskrivelse"
},
"profileFields": {
"name": "Rediger metadata"
},
"settings": {
"name": "Appinnstillinger"
},
"switch": {
"name": "Bytt konto"
}
},
"fontSize": {
"demo": "<p>Dette er en demotut😊. Du kan velge flere alternativer nedenfor.<br /><br />Denne innstillingen påvirker bare hovedinnholdet i tuter, men ikke andre skriftstørrelser.</p>",
"sizes": {
"S": "S",
"M": "M - Standard",
"L": "L",
"XL": "XL",
"XXL": "XXL"
}
},
"listAccounts": {
"heading": "Administrer brukere",
"error": "Slett bruker fra listen",
"empty": "Ingen bruker lagt til i denne listen"
},
"listEdit": {
"heading": "Rediger detaljer",
"title": "Emne",
"repliesPolicy": {
"heading": "Vis svar:",
"options": {
"none": "Ingen",
"list": "Medlemmer fra listen",
"followed": "Enhver fulgt bruker"
}
}
},
"listDelete": {
"heading": "Slett liste",
"confirm": {
"title": "Slett listen \"{{list}}\"?",
"message": "Denne handlingen kan ikke gjenopprettes."
}
},
"preferences": {
"visibility": {
"title": "Standard synlighet for post",
"options": {
"public": "Offentlig",
"unlisted": "Uoppført",
"private": "Kun følgere"
}
},
"sensitive": {
"title": "Marker alltid media som sensitivt"
},
"media": {
"title": "Visning av media",
"options": {
"default": "Skjul media som er merket som sensitivt",
"show_all": "Vis alltid media",
"hide_all": "Skjul alltid media"
}
},
"spoilers": {
"title": "Utvid alltid tuter som er merket med innholdsadvarsler"
},
"autoplay_gifs": {
"title": "Autostart GIF i tuter"
},
"filters": {
"title": "Innholdsfiltre",
"content": "{{count}} aktive"
},
"web_only": {
"title": "Oppdater innstillinger",
"description": "Innstillingene nedenfor kan kun oppdateres ved hjelp av web-grensesnittet"
}
},
"preferencesFilters": {
"expired": "Utløpt",
"keywords_one": "{{count}} nøkkelord",
"keywords_other": "{{count}} nøkkelord",
"statuses_one": "{{count}} tut",
"statuses_other": "{{count}} tut",
"context": "Gjelder i <0 />",
"contexts": {
"home": "følgende og lister",
"notifications": "varsler",
"public": "føderert",
"thread": "samtale",
"account": "profil"
}
},
"preferencesFilter": {
"name": "Navn",
"expiration": "Utløper",
"expirationOptions": {
"0": "Aldri",
"1800": "Etter 30 minutter",
"3600": "Etter 1 time",
"43200": "Etter 12 timer",
"86400": "Etter 1 dag",
"604800": "Etter 1 uke",
"18144000": "Etter 1 måned"
},
"context": "Gjelder i",
"contexts": {
"home": "Følgende og lister",
"notifications": "Varsel",
"public": "Føderert tidlinje",
"thread": "Samtalevisning",
"account": "Profil visning"
},
"action": "Når samsvarende",
"actions": {
"warn": "Kollapset, men kan vises",
"hide": "Skjult fullstendig"
},
"keywords": "Treff for disse nøkkelordene",
"keyword": "Nøkkelord",
"statuses": "Treff disse tutene"
},
"profile": {
"feedback": {
"succeed": "{{type}} oppdatert",
"failed": "{{type}} oppdatering feilet, prøv igjen"
},
"root": {
"name": {
"title": "Visningsnavn"
},
"avatar": {
"title": "Profilbilde",
"description": "Vil bli nedskalert til 400 x 400 px"
},
"header": {
"title": "Fane",
"description": "Vil bli nedskalert til 1500x500px"
},
"note": {
"title": "Beskrivelse"
},
"fields": {
"title": "Metadata",
"total_one": "{{count}} felt",
"total_other": "{{count}} felt"
},
"lock": {
"title": "Lås konto",
"description": "Krever at du godkjenner følgere manuelt"
},
"bot": {
"title": "Bot konto",
"description": "Denne kontoen utfører i hovedsak automatiserte handlinger og blir kanskje ikke holdt øye med"
}
},
"fields": {
"group": "Gruppe {{index}}",
"label": "Etikett",
"content": "Innhold"
},
"mediaSelectionFailed": "Bildebehandling mislyktes. Vennligst prøv igjen."
},
"push": {
"notAvailable": "Din telefon støtter ikke tooot's push-varsling",
"enable": {
"direct": "Aktiver push-varsler",
"settings": "Aktiver i innstillinger"
},
"missingServerKey": {
"message": "Feil på tjener for push-tjeneste",
"description": "Vennligst kontakt din serveradministrator for å konfigurere push-støtte"
},
"global": {
"heading": "Aktiver for {{acct}}",
"description": "Meldinger blir sendt gjennom toot's server"
},
"decode": {
"heading": "Meldingsdetaljer",
"description": "Meldinger sendt gjennom toots server er kryptert, men du kan velge å dekode meldingen på serveren. Vår server er åpen kildekode og har ingen logging."
},
"default": {
"heading": "Standard"
},
"follow": {
"heading": "Ny følger"
},
"follow_request": {
"heading": "Følgerforespørsel"
},
"favourite": {
"heading": "Favorisert"
},
"reblog": {
"heading": "Boostet"
},
"mention": {
"heading": "Nevnte deg"
},
"poll": {
"heading": "Oppdateringer om avstemning"
},
"status": {
"heading": "Tut fra påmeldte brukere"
},
"update": {
"heading": "%s er redigert"
},
"admin.sign_up": {
"heading": "Administrer: registrer deg"
},
"admin.report": {
"heading": "Administrer: rapporter"
},
"howitworks": "Lær hvordan ruting virker"
},
"root": {
"announcements": {
"content": {
"unread": "{{amount}} ulest",
"read": "Alt lest",
"empty": "Ingen"
}
},
"push": {
"content_true": "Aktivert",
"content_false": "Deaktivert"
},
"logout": {
"button": "Logg ut",
"alert": {
"title": "Logge ut?",
"message": "Etter å ha logget ut, må du logge på igjen",
"buttons": {
"logout": "Logg ut"
}
}
}
},
"settings": {
"theme": {
"heading": "Utseende",
"options": {
"auto": "Som system",
"light": "Lyst tema",
"dark": "Mørkt tema"
}
},
"darkTheme": {
"heading": "Mørkt tema",
"options": {
"lighter": "Standard",
"darker": "Helt svart"
}
},
"browser": {
"heading": "Åpning av lenke",
"options": {
"internal": "I appen",
"external": "Bruk nettleser"
}
},
"autoplayGifv": {
"heading": "Autostart GIF i tuter"
},
"feedback": {
"heading": "Funksjonsforespørsler"
},
"support": {
"heading": "Støtt tooot"
},
"contact": {
"heading": "Kontakt tooot"
},
"version": "Versjon v{{version}}",
"instanceVersion": "Mastodon versjon v{{version}}"
},
"switch": {
"existing": "Velg fra innlogget",
"new": "Logg inn på instans"
}
},
"shared": {
"account": {
"actions": {
"accessibilityLabel": "Handlinger for bruker {{user}}",
"accessibilityHint": "Du kan dempe, blokkere, rapportere eller dele denne brukeren"
},
"followed_by": " følger deg",
"moved": "Bruker flyttet",
"created_at": "Ble med: {{date}}",
"summary": {
"statuses_count": "{{count}} tut"
},
"toots": {
"default": "Tuter",
"all": "Tuter og svar"
},
"suspended": "Konto suspendert av moderatorene på serveren din"
},
"accountInLists": {
"name": "Lister av @{{username}}",
"inLists": "I liste",
"notInLists": "Andre lister"
},
"attachments": {
"name": "<0 /><1>'s media</1>"
},
"filter": {
"name": "Legg til filter",
"existed": "Finnes i disse filtrene"
},
"history": {
"name": "Rediger historikk"
},
"report": {
"name": "Rapporter {{acct}}",
"report": "Rapporter",
"forward": {
"heading": "Anonymt videresendt til ekstern server {{instance}}"
},
"reasons": {
"heading": "Hva skjer med denne kontoen?",
"spam": "Det er søppelpost",
"other": "Det er noe annet",
"violation": "Det bryter serverregler"
},
"comment": {
"heading": "Noe annet du vil legge til?"
},
"violatedRules": {
"heading": "Det bryter serverregler"
}
},
"search": {
"header": {
"prefix": "Søker",
"placeholder": "etter..."
},
"empty": {
"general": "Angi nøkkelord for å søke etter <bold>$t(screenTabs:shared.search.sections.accounts)</bold>、<bold>$t(screenTabs:shared.search.sections.hashtags)</bold> eller <bold>$t(screenTabs:shared.search.sections.statuses)</bold>",
"advanced": {
"header": "Avansert søk",
"example": {
"account": "$t(shared.search.header.prefix) $t(shared.search.sections.accounts)",
"hashtag": "$t(shared.search.header.prefix) $t(shared.search.sections.hashtags)",
"statusLink": "$t(shared.search.header.prefix) $t(shared.search.sections.statuses)",
"accountLink": "$t(shared.search.header.prefix) $t(shared.search.sections.accounts)"
}
},
"trending": {
"tags": "Populære emner"
}
},
"sections": {
"accounts": "Bruker",
"hashtags": "Emneknagg",
"statuses": "Tut"
},
"notFound": "Finner ikke <bold>{{searchTerm}}</bold> relatert til {{type}}",
"noResult": "Kan ikke finne noe, vennligst prøv et annet begrep"
},
"toot": {
"name": "Diskusjoner",
"remoteFetch": {
"title": "Inneholder eksternt innhold",
"message": "Føderert innhold er ikke alltid tilgjengelig på lokal forekomst. Dette innholdet hentes fra ekstern forekomst og merkes. Du kan samhandle med disse innholdet som vanlig."
}
},
"users": {
"accounts": {
"following": "Følger {{count}}",
"followers": "{{count}} følgere"
},
"statuses": {
"reblogged_by": "{{count}} boostet",
"favourited_by": "{{count}} som favoritt"
},
"resultIncomplete": "Resultater fra en ekstern instans er ufullstendig"
}
}
}

View File

@ -33,7 +33,7 @@
"poll": "Опитування, у якому ви голосували, закінчилося",
"reblog": {
"default": "{{name}} передмухує",
"myself": "",
"myself": "Передмухнуто мною",
"notification": "{{name}} передмухує ваш дмух"
},
"update": "Передмух був відредагований",

View File

@ -1,4 +1,5 @@
import Button from '@components/Button'
import GracefullyImage from '@components/GracefullyImage'
import haptics from '@components/haptics'
import { Loading } from '@components/Loading'
import { ParseHTML } from '@components/Parse'
@ -6,7 +7,6 @@ import RelativeTime from '@components/RelativeTime'
import CustomText from '@components/Text'
import { BlurView } from '@react-native-community/blur'
import { useAccessibility } from '@utils/accessibility/AccessibilityManager'
import { connectMedia } from '@utils/api/helpers/connect'
import { RootStackScreenProps } from '@utils/navigation/navigators'
import { useAnnouncementMutation, useAnnouncementQuery } from '@utils/queryHooks/announcement'
import { StyleConstants } from '@utils/styles/constants'
@ -22,7 +22,6 @@ import {
StyleSheet,
View
} from 'react-native'
import FastImage from 'react-native-fast-image'
import { FlatList, ScrollView } from 'react-native-gesture-handler'
import { SafeAreaView } from 'react-native-safe-area-context'
@ -139,12 +138,13 @@ const ScreenAnnouncements: React.FC<RootStackScreenProps<'Screen-Announcements'>
}
>
{reaction.url ? (
<FastImage
source={connectMedia({
uri: reduceMotionEnabled ? reaction.static_url : reaction.url
})}
style={{
width: StyleConstants.Font.LineHeight.M + 3,
<GracefullyImage
sources={{
default: { uri: reaction.url },
static: { uri: reaction.static_url }
}}
dimension={{
width: StyleConstants.Font.LineHeight.M,
height: StyleConstants.Font.LineHeight.M
}}
/>

View File

@ -9,10 +9,10 @@ import { ScreenComposeStackScreenProps } from '@utils/navigation/navigators'
import { getAccountStorage, setAccountStorage, useAccountStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { Image } from 'expo-image'
import React, { useContext, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Dimensions, Modal, Pressable, View } from 'react-native'
import FastImage from 'react-native-fast-image'
import ComposeContext from './utils/createContext'
import { formatText } from './utils/processText'
import { ComposeStateDraft, ExtendedAttachment } from './utils/types'
@ -140,7 +140,7 @@ const ComposeDraftsList: React.FC<ScreenComposeStackScreenProps<'Screen-Compose-
}}
>
{item.attachments.uploads.map((attachment, index) => (
<FastImage
<Image
key={index}
style={{
width:

View File

@ -11,10 +11,10 @@ import { featureCheck } from '@utils/helpers/featureCheck'
import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation'
import { useTheme } from '@utils/styles/ThemeManager'
import { Image } from 'expo-image'
import React, { RefObject, useContext, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { FlatList, Pressable, StyleSheet, View } from 'react-native'
import FastImage from 'react-native-fast-image'
import ComposeContext from '../../utils/createContext'
import { ExtendedAttachment } from '../../utils/types'
import chooseAndUploadAttachment from './addAttachment'
@ -104,9 +104,7 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => {
width: calculateWidth(item)
}}
>
<FastImage
enterTransition='fadeIn'
transitionDuration={60}
<Image
style={{ width: '100%', height: '100%' }}
source={
item.local?.thumbnail

View File

@ -193,10 +193,10 @@ const ScreenImagesViewer = ({
}}
>
<GracefullyImage
uri={{
preview: item.preview_url,
remote: item.remote_url,
original: item.url
sources={{
preview: { uri: item.preview_url },
default: { uri: item.url },
remote: { uri: item.remote_url }
}}
dimension={{
width:
@ -208,6 +208,7 @@ const ScreenImagesViewer = ({
? WINDOW_HEIGHT
: (WINDOW_WIDTH / imageWidth) * imageHeight
}}
enableLiveTextInteraction
/>
</View>
}

View File

@ -16,8 +16,8 @@ import { setAccountStorage, useAccountStorage, useGlobalStorage } from '@utils/s
import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation'
import { useTheme } from '@utils/styles/ThemeManager'
import * as Crypto from 'expo-crypto'
import * as Notifications from 'expo-notifications'
import * as Random from 'expo-random'
import * as WebBrowser from 'expo-web-browser'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -164,16 +164,15 @@ const TabMePush: React.FC = () => {
url: `push/unsubscribe/${pushPath}`
})
setAccountStorage([{ key: 'push', value: { ...push, global: false } }])
if (Platform.OS === 'android') {
Notifications.deleteNotificationChannelGroupAsync(accountFull)
}
setAccountStorage([{ key: 'push', value: { ...push, global: false } }])
} else {
// Fix a bug for some users of v4.8.0
let authKey = push.key
if (push.key?.length <= 10) {
authKey = fromByteArray(Random.getRandomBytes(16))
authKey = fromByteArray(Crypto.getRandomBytes(16))
}
// Turning on
const randomPath = (Math.random() + 1).toString(36).substring(2)
@ -182,7 +181,7 @@ const TabMePush: React.FC = () => {
const body: {
subscription: any
alerts: Mastodon.PushSubscription['alerts']
data: { alerts: Mastodon.PushSubscription['alerts'] }
} = {
subscription: {
endpoint,
@ -192,7 +191,7 @@ const TabMePush: React.FC = () => {
auth: authKey
}
},
alerts: push.alerts
data: { alerts: push.alerts }
}
const res = await apiInstance<Mastodon.PushSubscription>({
@ -239,7 +238,6 @@ const TabMePush: React.FC = () => {
setAccountStorage([
{ key: 'push', value: { ...push, global: true, key: authKey } }
])
if (Platform.OS === 'android') {
setChannels(true)
}

View File

@ -5,13 +5,13 @@ import { LOCALES } from '@i18n/locales'
import { useNavigation } from '@react-navigation/native'
import { connectVerify } from '@utils/api/helpers/connect'
import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles'
import { GLOBAL } from '@utils/storage'
import { useGlobalStorage } from '@utils/storage/actions'
import { useTheme } from '@utils/styles/ThemeManager'
import * as Localization from 'expo-localization'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Linking, Platform } from 'react-native'
import { GLOBAL } from '../../../../App'
import { mapFontsizeToName } from '../SettingsFontsize'
const SettingsApp: React.FC = () => {

View File

@ -82,15 +82,25 @@ const AccountAttachments: React.FC = () => {
} else {
return (
<GracefullyImage
uri={{
original:
item.media_attachments[0]?.preview_url || item.media_attachments[0]?.url,
remote: item.media_attachments[0]?.remote_url
sources={{
preview: {
uri: item.media_attachments[0]?.preview_url,
width: item.media_attachments[0]?.meta?.small?.width,
height: item.media_attachments[0]?.meta?.small?.height
},
default: {
uri: item.media_attachments[0]?.url,
width: item.media_attachments[0]?.meta?.original?.width,
height: item.media_attachments[0]?.meta?.original?.height
},
remote: {
uri: item.media_attachments[0]?.remote_url,
width: item.media_attachments[0]?.meta?.original?.width,
height: item.media_attachments[0]?.meta?.original?.height
},
blurhash: item.media_attachments[0]?.blurhash
}}
blurhash={
item.media_attachments[0] && (item.media_attachments[0].blurhash || undefined)
}
dimension={{ width: width, height: width }}
dimension={{ width, height: width }}
style={{ marginLeft: StyleConstants.Spacing.Global.PagePadding }}
onPress={() => navigation.push('Tab-Shared-Toot', { toot: item })}
dim

View File

@ -1,6 +1,5 @@
import GracefullyImage from '@components/GracefullyImage'
import navigationRef from '@utils/navigation/navigationRef'
import { useGlobalStorage } from '@utils/storage/actions'
import React, { useContext } from 'react'
import { Dimensions, Image } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
@ -11,11 +10,9 @@ const AccountHeader: React.FC = () => {
const topInset = useSafeAreaInsets().top
useGlobalStorage.string('account.active')
return (
<GracefullyImage
uri={{ original: account?.header, static: account?.header_static }}
sources={{ default: { uri: account?.header }, static: { uri: account?.header_static } }}
style={{ height: Dimensions.get('window').width / 3 + topInset }}
onPress={() => {
if (account) {

View File

@ -2,7 +2,7 @@ import Button from '@components/Button'
import menuAt from '@components/contextMenu/at'
import { RelationshipOutgoing } from '@components/Relationship'
import { useNavigation } from '@react-navigation/native'
import { useAccountStorage } from '@utils/storage/actions'
import { checkIsMyAccount } from '@utils/helpers/isMyAccount'
import { StyleConstants } from '@utils/styles/constants'
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
@ -56,12 +56,11 @@ const AccountInformationActions: React.FC = () => {
)
}
const [accountId] = useAccountStorage.string('auth.account.id')
const ownAccount = account?.id === accountId
const isMyAccount = checkIsMyAccount(account?.id)
const mAt = menuAt({ account })
if (!ownAccount && account) {
if (!isMyAccount && account) {
return (
<View style={styles.base}>
{relationship && !relationship.blocked_by ? (

View File

@ -3,7 +3,6 @@ import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import navigationRef from '@utils/navigation/navigationRef'
import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { useAccountStorage } from '@utils/storage/actions'
import { StyleConstants } from '@utils/styles/constants'
import React, { useContext } from 'react'
import AccountContext from '../Context'
@ -13,20 +12,13 @@ const AccountInformationAvatar: React.FC = () => {
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const [accountAvatarStatic] = useAccountStorage.string('auth.account.avatar_static')
return (
<GracefullyImage
key={account?.avatar}
style={{
borderRadius: 8,
overflow: 'hidden',
width: StyleConstants.Avatar.L,
height: StyleConstants.Avatar.L
}}
uri={{
original: account?.avatar || (pageMe ? accountAvatarStatic : undefined),
static: account?.avatar_static || (pageMe ? accountAvatarStatic : undefined)
style={{ borderRadius: 8, overflow: 'hidden' }}
dimension={{ width: StyleConstants.Avatar.L, height: StyleConstants.Avatar.L }}
sources={{
default: { uri: account?.avatar },
static: { uri: account?.avatar_static }
}}
onPress={() => {
if (account) {

View File

@ -248,7 +248,11 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
body: data.map(remote => {
const localMatch = old?.pages[0].body.find(local => local.uri === remote.uri)
if (localMatch) {
return { ...localMatch, _level: remote._level }
return {
...localMatch,
_level: remote._level,
key: `${localMatch.id}_remote`
}
} else {
return appendRemote.status(remote)
}
@ -275,6 +279,7 @@ const TabSharedToot: React.FC<TabSharedStackScreenProps<'Tab-Shared-Toot'>> = ({
ref={flRef}
windowSize={5}
data={query.data?.pages?.[0].body}
extraData={query.dataUpdatedAt}
renderItem={({ item, index }) => {
const prev = query.data?.pages[0].body[index - 1]?._level || 0
const curr = item._level || 0

View File

@ -50,9 +50,9 @@ const ScreenTabs = () => {
return <Icon name='bell' size={size} color={color} />
case 'Tab-Me':
return (
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<View key={avatarStatic} style={{ flexDirection: 'row', alignItems: 'center' }}>
<GracefullyImage
uri={{ original: avatarStatic }}
sources={{ default: { uri: avatarStatic } }}
dimension={{ width: size, height: size }}
style={{
borderRadius: size,

View File

@ -1,5 +1,5 @@
import { GLOBAL } from '@utils/storage'
import axios from 'axios'
import { GLOBAL } from '../../App'
import { ctx, handleError, PagedResponse, parseHeaderLinks, userAgent } from './helpers'
import { CONNECT_DOMAIN } from './helpers/connect'

View File

@ -1,9 +1,9 @@
import { mapEnvironment } from '@utils/helpers/checkEnvironment'
import { GLOBAL } from '@utils/storage'
import { setGlobalStorage } from '@utils/storage/actions'
import axios from 'axios'
import parse from 'url-parse'
import { userAgent } from '.'
import { GLOBAL } from '../../../App'
const list = [
'n61owz4leck',
@ -81,19 +81,18 @@ export const CONNECT_DOMAIN = (index?: number) =>
development: 'connect-development.tooot.app'
})
export const connectMedia = ({
uri
}: {
export const connectMedia = (args?: {
uri?: string
}): { uri?: string; headers?: { 'x-tooot-domain': string } } => {
if (GLOBAL.connect) {
if (uri) {
const host = parse(uri).host
if (args?.uri) {
const host = parse(args.uri).host
return {
uri: uri.replace(
...args,
uri: args.uri.replace(
host,
CONNECT_DOMAIN(
uri
args.uri
.split('')
.map(i => i.charCodeAt(0))
.reduce((a, b) => a + b, 0) %
@ -103,10 +102,10 @@ export const connectMedia = ({
headers: { 'x-tooot-domain': host }
}
} else {
return { uri }
return { ...args }
}
} else {
return { uri }
return { ...args }
}
}

View File

@ -1,10 +1,10 @@
import * as Sentry from '@sentry/react-native'
import { GLOBAL } from '@utils/storage'
import { setGlobalStorage } from '@utils/storage/actions'
import chalk from 'chalk'
import Constants from 'expo-constants'
import { Platform } from 'react-native'
import parse from 'url-parse'
import { GLOBAL } from '../../../App'
const userAgent = {
'User-Agent': `tooot/${Constants.expoConfig?.version} ${Platform.OS}/${Platform.Version}`

View File

@ -1,7 +1,7 @@
import { GLOBAL } from '@utils/storage'
import { getAccountDetails } from '@utils/storage/actions'
import { StorageGlobal } from '@utils/storage/global'
import axios, { AxiosRequestConfig } from 'axios'
import { GLOBAL } from '../../App'
import { ctx, handleError, PagedResponse, parseHeaderLinks, userAgent } from './helpers'
import { CONNECT_DOMAIN } from './helpers/connect'

View File

@ -0,0 +1,9 @@
import { getAccountStorage } from '@utils/storage/actions'
export const checkIsMyAccount = (id?: Mastodon.Account['id']): boolean => {
if (!id) return false
const accountId = getAccountStorage.string('auth.account.id')
const accountDomain = getAccountStorage.string('auth.account.domain')
return accountId === id || `${accountId}@${accountDomain}` === id
}

View File

@ -9,7 +9,7 @@ export const urlMatcher = (
):
| {
domain: string
account?: Partial<Pick<Mastodon.Account, 'id' | 'acct' | '_remote'>>
account?: Partial<Pick<Mastodon.Account, 'acct' | '_remote'>>
status?: Partial<Pick<Mastodon.Status, 'id' | '_remote'>>
}
| undefined => {
@ -24,13 +24,18 @@ export const urlMatcher = (
const _remote = parsed.hostname !== getAccountStorage.string('auth.domain')
let statusId: string | undefined
let accountId: string | undefined
let accountAcct: string | undefined
const segments = parsed.pathname.split('/')
const last = segments[segments.length - 1]
const length = segments.length // there is a starting slash
const testAndAssignStatusId = (id: string) => {
if (!!parseInt(id)) {
statusId = id
}
}
switch (last?.startsWith('@')) {
case true:
if (length === 2 || (length === 3 && segments[length - 2] === 'web')) {
@ -45,14 +50,14 @@ export const urlMatcher = (
if (nextToLast === 'statuses') {
if (length === 4 && segments[length - 3] === 'web') {
// https://social.xmflsct.com/web/statuses/105590085754428765 <- old
statusId = last
testAndAssignStatusId(last)
} else if (
length === 5 &&
segments[length - 2] === 'statuses' &&
segments[length - 4] === 'users'
) {
// https://social.xmflsct.com/users/tooot/statuses/105590085754428765 <- default Mastodon
statusId = last
testAndAssignStatusId(last)
// accountAcct = `@${segments[length - 3]}@${domain}`
}
} else if (
@ -61,7 +66,7 @@ export const urlMatcher = (
) {
// https://social.xmflsct.com/web/@tooot/105590085754428765 <- pretty Mastodon v3.5 and below
// https://social.xmflsct.com/@tooot/105590085754428765 <- pretty Mastodon v4.0 and above
statusId = last
testAndAssignStatusId(last)
// accountAcct = `${nextToLast}@${domain}`
}
}
@ -70,7 +75,7 @@ export const urlMatcher = (
return {
domain,
...((accountId || accountAcct) && { account: { id: accountId, acct: accountAcct, _remote } }),
...(accountAcct && { account: { acct: accountAcct, _remote } }),
...(statusId && { status: { id: statusId, _remote } })
}
}

View File

@ -25,7 +25,7 @@ const accountQueryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyA
const match = urlMatcher(key.url)
const domain = match?.domain
const id = key.id || match?.account?.id
const id = key.id
const acct = key.acct || key.username || match?.account?.acct
if (!key._local && domain) {

View File

@ -244,6 +244,12 @@ export const setAccount = async (account: string) => {
return
}
log('log', 'setAccount', `binding storage of ${account}`)
storage.account = temp
setGlobalStorage('account.active', account)
await queryClient.resetQueries()
queryClient.clear()
await apiGeneral<Mastodon.Account>({
method: 'get',
domain,
@ -254,15 +260,10 @@ export const setAccount = async (account: string) => {
})
.then(res => res.body)
.then(async a => {
temp.set('auth.account.acct', a.acct)
temp.set('auth.account.avatar_static', a.avatar_static)
storage.account?.set('auth.account.acct', a.acct)
storage.account?.set('auth.account.avatar_static', a.avatar_static)
log('log', 'setAccount', `binding storage of ${account}`)
await queryClient.resetQueries()
queryClient.clear()
storage.account = temp
setGlobalStorage('account.active', account)
log('log', 'setAccount', 'update details')
})
.catch(async error => {
if (error?.status && error.status == 401) {
@ -302,7 +303,7 @@ export const removeAccount = async (account: string, warning: boolean = true) =>
apiGeneral({
method: 'post',
domain: revokeDetails.domain,
url: '/oauth/revoke',
url: 'oauth/revoke',
body: revokeDetails
})
}

View File

@ -4,3 +4,7 @@ import { MMKV } from 'react-native-mmkv'
export const storage: { global: MMKV; account?: MMKV } = { global: new MMKV(), account: undefined }
export const secureStorage = createSecureStore()
export const GLOBAL: { connect?: boolean } = {
connect: undefined
}

695
yarn.lock

File diff suppressed because it is too large Load Diff