Today I Learned

A Hashrocket project

82 posts by dillonhafer

Expo PushNotifications with pop-ups on Android

Expo has an amazing push notification service. In order to display push notifications in a pop-up style (the default on iOS) on Android, you must do two things:

  1. Create an Android push notification channel
  2. Set the priority and channelId in the push notification payload to expo.

Create channel example:

import React from "react";
import { Platform } from "react-native";
import { Notifications } from "expo";

class App extends React.Component<Props> {
  componentDidMount() {
    if (Platform.OS === "android") {
      Notifications.createChannelAndroidAsync("chat-messages", {
        name: "Messages",
        priority: "max",
        sound: true,
        vibrate: [0, 250, 500, 250]
      });
    }
  }

  render() {
    // ...
  }
}

Send PN with priority and channel example:

curl -H "Content-Type: application/json" -X POST "https://exp.host/--/api/v2/push/send" -d '{
  "to": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
  "title":"New Message",
  "body": "Are you there?",
  "channelId": "chat-messages",
  "priority": "max"
}'

Prefetch images in ReactNative

When a react native app boots and there is a known set of user/group avatar urls received from an API, it may be a better user experience to load them before hand.

React Native’s Image component has a prefetch method that will save the image into the device’s image cache:

import { Image } from "react-native";

const App = () => {
  const { users, loading } = useUserFetcher();

  users.forEach(u => Image.prefetch(u.avatarURL));

  return (
    <View>
      <UserList users={users} loading={loading} />
    </View>
  );
};

RN fetch on Android requires mime type

ReactNative on Android requires a mime type when uploading files with FormData. But the only way to get the mime type of a user-chosen file is to read the file or files into memory after a user select one or more. In all of my use cases, my server didn’t care if the mimetype was in the FormData.

The easy solution is to just set the mimetype to a binary file type (e.g. application/octet-stream):

let data = new FormData();
data.append("file", {
  name: "cool.pdf",
  uri: "///files/cool.pdf",
  // MIME-type required on Android
  type: "application/octet-stream"
});

fetch("http://example.com/upload-file", data);

Ecto queries can be combined

Queries can take other queries to build on top of, which is very useful for conditional query logic:

def find_posts(group_id, title) do
  posts =
    from(posts in Phoenix.Post,
      where: posts.group_id == ^group_id,
    )
    |> title_search(title)
    |> Phoenix.Repo.all()
end

defp title_search(query, title) do
  query
    |> where([posts], posts.title == ^title)
end

defp title_search(query, nil) do
  query
end

💅 styled-components limit prop names

💅 styled-components is one of my absolute favorite libraries! ❤️ I just have to be careful not use prop names for my components that could be interpreted as styles.

For example:

interface Props {
  position: "left-side" | "right-side";
  item: Item;
}

const ListItem= styled.View<{position: "left-side" | "right-side"}>(({position}) => ({
  backgroundColor: position === "left-side" ? "burlywood" : "skyblue";
}));

const ListItemContainer: React.FC<Props> = ({position, item}) => {
  return (
    <ListItem position={position}>
      <Text>{item.name}</Text>
    </ListItem>
  );
}

<ListItemContainer item={item} position="left-side" />

This will return an error because in StyleSheet, position must be one of absolute or relative

How to manually edit ufw rules

Usually you don’t want to manually edit ufw, but in this case I just needed to update an ip address across multiple ip4 and ip6 rules. Turns out there is config file that can be very carefully edited:

$ sudo vim /lib/ufw/user.rules
$ sudo vim /lib/ufw/user6.rules

Very carefully update the ip addresses and then reload ufw:

$ sudo ufw reload

See git history of a renamed file

If you rename a file, git won’t show history of the previous name:

$ git log --pretty=oneline things/text.txt
8567d... Move file into things directory

However, the --follow flag will allow you to see the history of commits beyond the rename:

$ git log --follow --pretty=oneline things/text.txt
8567d... Move file into things directory
1458a... Fix something
0aac5... Add something

Docs:

--follow
  Continue listing the history of a file beyond renames (works only for a single file).

How to typescript react native list refs

VirtualLists (i.e. SectionList, FlatList) have always been dificult for me to describe in typescript, but the following seems to work:

interface Props {
  list: MutableRefObject<SectionList<any> | undefined>;
}

Example:

import React, { MutableRefObject } from "react";
import { SectionList, View, TouchableOpacity, Text } from "react-native";

interface Props {
  listRef: MutableRefObject<SectionList<any> | undefined>;
}

const Button = (props: Props) => {
  return (
    <TouchableOpacity
      onPress={() => {
        props.listRef.current?.scrollToLocation({
          itemIndex: 0,
          sectionIndex: 0,
          animated: true
        });
      }}
    >
      <Text>Scroll Up</Text>
    </TouchableOpacity>
  );
};

const Screen = () => {
  const listRef = useRef<SectionList<any>>();

  return (
    <View>
      <SectionList ref={listRef} />
      <Button listRef={listRef} />
    </View>
  );
};

How to redirect standard error

2> Allows us to redirect standard error.

Taking advantage of rm’s ability to not delete files recursively can come in handy, especially when writing clean up scripts, but it can be noisy when you don’t care.

Give this file structure:

~/
  tmpfile1.txt
  tmpfile2.txt
  tmpfile3.txt
  tmpfile4.txt
  do-not-delete/
    secrets.yml

I may want to delete all the files, but not touch the directories (to keep file removal simple)

$ rm *
rm: cannot remove 'do-not-delete': Is a directory

$ ls
~/
  do-not-delete/
    secrets.yml

Because the directory error message comes over stderr, we can simply redirect it to /dev/null to ignore it:

rm * 2> /dev/null

How to only stage deleted files

If I delete a bunch of files and just want to stage the deleted ones, I can use xargs to add them:

$ git ls-files --deleted | xargs git add

Example output:

$ git status

  deleted: lib/to/deleted_1.txt
  deleted: lib/to/deleted_2.txt
  modified lib/that/changed_1.txt
  deleted: lib/to/deleted_3.txt
  deleted: lib/to/deleted_4.txt
  modified lib/that/changed_2.txt
  deleted: lib/to/deleted_5.txt
  modified lib/that/changed_3.txt
$ git ls-files --deleted | xargs git add
Changes to be committed:

  deleted: lib/to/deleted_1.txt
  deleted: lib/to/deleted_2.txt
  deleted: lib/to/deleted_3.txt
  deleted: lib/to/deleted_4.txt
  deleted: lib/to/deleted_5.txt

Changes not staged for commit:

  modified lib/that/changed_1.txt
  modified lib/that/changed_2.txt
  modified lib/that/changed_3.txt

How to change the placeholder text color

The TextInput component in react-native has a property of placeholderTextColor that allows you to configure the placeholder text color! However, react-native-web does not have this property and it must be done in css. So if you were using react-native-web you would need to include .css file:

// screen.tsx
import "./styles.css";
import { TextInput } from "react-native";

const Screen = () => (
  <TextInput
    testID="change-placeholder-color"
    placeholder="enter some text"
    defaultValue=""
  />
);
/************* 
*
*  styles.css
*
**************/
[data-testid="change-placeholder-color"]::-webkit-input-placeholder {
  color: skyblue;
}
[data-testid="change-placeholder-color"]::-moz-placeholder {
  color: skyblue;
}
[data-testid="change-placeholder-color"]::-ms-placeholder {
  color: skyblue;
}
[data-testid="change-placeholder-color"]::placeholder {
  color: skyblue;
}

Browsers have a Web Cryptography API

All major versions of browser implement a Web Cryptography API for obtaining random numbers:

const numbers = new Uint32Array(1);
window.crypto.getRandomValues(numbers);
console.log(numbers);
// => [1627205277]

However, the methods are overridable, not read-only, and are vulnerable to polyfill attacks. This shouldn’t be used yet and is still being developed, but I found out the API exists.

How to remove unused deps from mix.lock

When you remove a dep from the mix.exs file it will remain in the mix.lock file. Unused deps need to be “unlocked” in order to be removed from the lock file.

For example:

mix deps.clean --unused

will only remove the unused dependencies’ files, but will keep the library in the mix.lock file. If you run the clean command the --unlock option, it will also remove it from the lock file.

For example:

mix deps.clean --unlock --unused

What a cursor is in postgres

SQL has a structure called a CURSOR that, according to the docs:

Rather than executing a whole query at once, read the query result a few rows at a time

This is mainly for solving memory usage issues, but probably not very applicable to web applications. Here’s an example syntax to periodically fetch a limited amount of rows:

begin;
declare posts_cursor cursor for select * from posts;
fetch 10 from posts_cursors;
fetch 10 from posts_cursors;
commit;

This is not advised due to leaving a transaction open, but a simple example. More powerful use cases would be iterating a query in a function for updating a small amount of records at a time.

How to change the request body size in Phoenix

Plug.Parsers accepts a length: key with a value of approximate bytes to accept and can be used in an endpoint file.

# endpoint.ex
plug Plug.Parsers,
  pasrsers: [:urlencoded, :multipart],
  length: 100_000_000 # 100 MB body size (approximately)

You can also configure the length for a specific parser:

plug Plug.Parsers,
  parsers: [:urlencoded, {:multipart, length: 100_000_000}] # 100 MB body size (approximately)

Ecto can only execute 1 sql statement at a time

Ecto can only ever execute 1 sql statement at a time, by design. For performance concerns, every statement is wrapped in a prepared statement
*some “security” is a fringe benefit of prepare statements.

In regards to the performance concerns of the prepared statement, PostgreSQL will force re-analysis of the statement when the objects in the statement have undergone definitional changes (DDL), making its use in a migration useless.

An example of a migration if you need to perform multiple statements:

def up do
  execute("create extension if not exists \"uuid-ossp\";")
  execute("alter table schedules add column user_id uuid;")
  execute("create unique index on schedules (id, user_id);")
end

def down do
  execute("alter table schedules drop column user_id;")
  execute("drop extension if exists \"uuid-ossp\";")
end

Convert .mov to .gif with ffmpeg

Sometimes putting a gif in a pull request is really helpful for reviewers. If you’ve recorded a movie on an iOS simulator with xcrun simctl or just QuickTime, it’s very simple to convert them to animated .gifs

Example:

ffmpeg -i screen_recording.mov \
       -s 415x925 \
       -pix_fmt rgb24 \
       -r 10 \
       -f gif \
       screen_recording.gif

Now it would be nice to have a function that could extract the video dimensions automatically 🤔 mdls 🤯

1 function gif-mov() {
2   movie=$1
3   height=$(mdls -name kMDItemPixelHeight ${movie} | grep -o '[0-9]\+')
4   width=$(mdls -name kMDItemPixelWidth ${movie} | grep -o '[0-9]\+')
5   dimensions="${width}x${height}"
6   ffmpeg -i ${movie} -s ${dimensions} -pix_fmt rgb24 -r 10 -f gif ${movie}.gif
7 }

Usage:

$ gif-mov ~/Desktop/cool-screen-recording.mov

Tuples are never inferred in TypeScript

If I have a function that takes a number tuple:

type Options = {
  aspect: [number, number],
  ...
}

const pickImage = (imageOptions: Options) => (
  ...
)

This will give a type error:

const myOptions = {
  aspect: [4, 3],
};

// ❌ Expected [number, number], got number[]
pickImage(myOptions);

I must use type assertion when passing tuples:

const myOptions = {
  aspect: [4, 3] as [number, number],
};

pickImage(myOptions);

Apollo-React hooks can easily refetch queries

React-apollo hooks allow you to easily refetch queries after a mutation, which is useful for updating a list when you have create/update/delete mutations.

useMutation takes an argument called refetchQueries that will run queries named in the array.

import React from "react";
import { useMutation } from "@apollo/react-hooks";
import gql from "graphql-tag";
import { Button } from "react-native";
import { Item } from "./types";

const ITEM_DELETE = gql`
  mutation ItemDelete($id: ID!) {
    itemDelete(id: $id) {
      id
    }
  }
`;

interface Props {
  item: Item;
}

const DeleteItemButton = ({item}: Props) => {
  const [deleteItem] = useMutation(ITEM_DELETE, {
    variables: { id: item.id },
    refetchQueries: ["GetItemList"],
  });

  return (
    <Button title="Delete" onPress={() => deleteItem()} />
  )
}

Push git branch to another machine

Whenever I’m working in the same git repo on multiple machines, and I need to work on a branch on both machines I usually push the branch to a shared remote for the sole purpose of pulling it down on the other machine. This third machine can be avoided by push directly between machines:

$ myproject(feature-branch): git remote add machine2 ssh://machine2:/Users/dillon/dev/myproject
$ myproject(feature-branch): git push machine2 feature-branch

Over on machine2:

$ myproject(master): git branch
* master
  feature-branch       <= 😯

How to expose entire Swift class to Objective-C

Use the @objcMembers annotation in your Swift class. When working on a project that is migrating from Objective-C to Swift you will most likely be sharing all Swift functions with Objective-C, but that could start to look ugly:

class MyClass: NSObject {
  @objc func fancy() -> String {
    return "fancy"
  }
  
  @objc func tooFancy() -> String {
    return "🤵"
  }
}

But if everything needs to be available to Objective-C, we simply do:

@objcMembers
class MyClass: NSObject {
  func fancy() -> String {
    return "fancy"
  }
  
  func tooFancy() -> String {
    return "🤵"
  }
}

How to remove NotificationCenter observer in Swift

Use the method deinit. In Objective-C classes I would always remove NSNotificationCenter observers in the -dealloc method, but a Swift class doesn’t have a -dealloc method. Instead, Swift has a deinit method.

Here is an example of Objective-C:

@implementation MyClass
  - (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
  }
@end

And the equivilant in Swift:

class MyClass {
  deinit {
    NotificationCenter.default.removeObserver(self)
  }
}

Open iOS simulator app data directory

If you need to quickly open an app’s document directory on the iOS Simulator, you can quickly get the path from the xcrun CLI.

xcrun simctl get_app_container booted ${bundle_id} data

Here’s some ruby you can throw into a Rakefile of your iOS app 😉

# Rakefile
desc "Open the simulator document directory"
task :docs do
  bundle_id = File.read("MyAppName/MyAppName.xcodeproj/project.pbxproj")
    .scan(/PRODUCT_BUNDLE_IDENTIFIER = "(.*)"/)
    .flatten.first

  app_directory = `xcrun simctl get_app_container booted #{bundle_id} data`

  puts "Opening simulator directory: #{app_directory}"
  `open #{app_directory}`
end

🔍 Using NSArray with CONTAINS NSPredicates

NSPredicate’s predicateWithFormat method takes a va_list of arguments, so it’s not possible to pass an array to your format string. But, the same result can be achieved by combining multiple NSPredicates together using an NSCompoundPredicate:

Given a space-separated array of search words:

NSArray<NSString*> *words = [@"my search terms" componentsSeparatedByString:@" "];

You can combine them by first creating multiple predicates:

NSMutableArray<NSPredicate *> *predicates = [NSMutableArray new];
for (NSString *word in words) {
  NSPredicate *predicate = [NSPredicate predicateWithFormat:@"attribute CONTAINS[cd] %@", word];
  [predicates addObject:predicate];
}

And finally create one NSPredicate via NSCompoundPredicate

NSPredicate *finalPredicate =
        [NSCompoundPredicate andPredicateWithSubpredicates:predicates];

Shell parameter expansion

Shells can perform variable expansion, this is really useful for default argument variables in shell functions.

Here is a simple example of assigning a variable from an argument or defaulting to running a different shell command:

clog() {
  log_date=${1-`date +%Y-%m-%d`}
  git log --after="$log_date 00:00" --before="$log_date 23:59"
}

This allows me to write

clog to default to today’s date or

clog 2018-10-02 to look at the logs from yesterday

The date in the example clog 2018-10-02 would come through as $1 in the function, so ${1-`date`} means run this shell command if there is no $1.

*references:

https://www.tldp.org/LDP/abs/html/parameter-substitution.html#USAGEMESSAGE

Client Connection Vars with Ecto

Postgres has dozens of connection variables it will take, a couple of my favorite ones are statement_timeout and application_name.

Ecto takes a parameters connection option, which is a keyword list of connection parameters.

# dev.exs

...

# Configure your database
config :myapp, Myapp.Repo,
  adapter: Ecto.Adapters.Postgres,
  database: "myapp_dev",
  socket_dir: "/var/run/postgresql",
  parameters: [application_name: "My App Development", statement_timeout: "5000"],
  pool_size: 10



Example of statement_timeout being respected in Ecto:

Ecto.Adapters.SQL.query!(Myapp.Repo, "select pg_sleep(86400)")

** (Postgrex.Error) ERROR 57014 (query_canceled): canceling statement due to statement timeout
    (ecto) lib/ecto/adapters/sql.ex:200: Ecto.Adapters.SQL.query!/5


Client Connection Variables references:https://www.postgresql.org/docs/current/static/runtime-config-client.html

Use a unix socket with Ecto

Ecto allows the use of a unix socket to connect to postgres, instead of TCP. In order to use a unix socket in a exs file, you should remove the hostname list item, and provide the socket_dir list item.

# dev.exs

...

# Configure your database
config :myapp, Myapp.Repo,
  adapter: Ecto.Adapters.Postgres,
  database: "myapp_dev",
  socket_dir: "/var/run/postgresql",
  pool_size: 10

NODE_OPTIONS without node

I was using storybook and found it had grown so large that its node process would run out of memory before it could start. The downside was that the storybook command did not accept node arguments, like --max_old_space_size=4096

Starting with node 8, node will look for options in an environment variable named NODE_OPTIONS to be interpreted as if they had been specified on the command line before the actual command line (so they can be overridden).

Example to give storybook more memory:

NODE_OPTIONS=--max_old_space_size=4096 storybook start

*References:
https://storybook.js.org
https://nodejs.org/dist/latest-v8.x/docs/api/cli.html#cli_node_options_options

🆙 Update macOS from cli

Opening the App Store and navigating to the update tab and clicking and confirming the update buttons is just too many steps for me.

macOS has a cli to the software update process:

$ /usr/sbin/softwareupdate

With some fancy arguments I can update everything in one command:

$ sudo softwareupdate -ia

the -ia flag means install all the available updates.